fix: editor document to html encoder added (#175)

* fix:added html document encoder

* code optimization

* code refactored

* correction of the html attributes variable
This commit is contained in:
Ali Hassan 2023-06-09 11:30:29 +05:00 • committed by GitHub
commit 6677e2ae62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 949 additions and 518 deletions

View file

@ -64,4 +64,5 @@ export 'src/core/document/deprecated/node.dart';
export 'src/core/document/deprecated/document.dart'; export 'src/core/document/deprecated/document.dart';
export 'src/plugins/html/html_document_decoder.dart'; export 'src/plugins/html/html_document_decoder.dart';
export 'src/plugins/html/html_document_encoder.dart';
export 'src/plugins/html/html_document.dart'; export 'src/plugins/html/html_document.dart';

View file

@ -1,6 +1,5 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:html/parser.dart' show parse; import 'package:html/parser.dart' show parse;
import 'package:html/dom.dart' as dom; import 'package:html/dom.dart' as dom;
@ -31,7 +30,11 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
final localName = domNode.localName; final localName = domNode.localName;
if (HTMLTags.formattingElements.contains(localName)) { if (HTMLTags.formattingElements.contains(localName)) {
final attributes = _parserFormattingElementAttributes(domNode); final attributes = _parserFormattingElementAttributes(domNode);
delta.insert(domNode.text, attributes: attributes); nodes.add(
paragraphNode(
delta: Delta()..insert(domNode.text, attributes: attributes),
),
);
} else if (HTMLTags.specialElements.contains(localName)) { } else if (HTMLTags.specialElements.contains(localName)) {
nodes.addAll( nodes.addAll(
_parseSpecialElements( _parseSpecialElements(
@ -69,11 +72,16 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
case HTMLTags.orderedList: case HTMLTags.orderedList:
return _parseOrderListElement(element); return _parseOrderListElement(element);
case HTMLTags.list: case HTMLTags.list:
return _parseListElement(element, type: type); return [
_parseListElement(
element,
type: type,
)
];
case HTMLTags.paragraph: case HTMLTags.paragraph:
return [_parseParagraphElement(element)]; return [_parseParagraphElement(element)];
case HTMLTags.blockQuote: case HTMLTags.blockQuote:
return [_parseBlockQuoteElement(element)]; return _parseBlockQuoteElement(element);
case HTMLTags.image: case HTMLTags.image:
return [_parseImageElement(element)]; return [_parseImageElement(element)];
default: default:
@ -113,7 +121,6 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
} }
break; break;
default: default:
assert(false, 'Unknown formatting element: $element');
break; break;
} }
for (final child in element.children) { for (final child in element.children) {
@ -133,34 +140,37 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
); );
} }
Node _parseBlockQuoteElement(dom.Element element) => quoteNode( Iterable<Node> _parseBlockQuoteElement(dom.Element element) {
delta: Delta()..insert(element.text), return element.children
); .map((child) => _parseListElement(child, type: QuoteBlockKeys.type))
.toList();
}
Iterable<Node> _parseUnOrderListElement(dom.Element element) { Iterable<Node> _parseUnOrderListElement(dom.Element element) {
final result = <Node>[]; return element.children
for (final child in element.children) { .map(
result.addAll(_parseListElement(child, type: NumberedListBlockKeys.type)); (child) => _parseListElement(child, type: BulletedListBlockKeys.type),
} )
return result; .toList();
} }
Iterable<Node> _parseOrderListElement(dom.Element element) { Iterable<Node> _parseOrderListElement(dom.Element element) {
final result = <Node>[]; return element.children
for (final child in element.children) { .map(
result.addAll(_parseListElement(child, type: NumberedListBlockKeys.type)); (child) => _parseListElement(child, type: NumberedListBlockKeys.type),
} )
return result; .toList();
} }
Iterable<Node> _parseListElement( Node _parseListElement(
dom.Element element, { dom.Element element, {
required String type, required String type,
}) { }) {
final delta = _parseDeltaElement(element); final delta = _parseDeltaElement(element);
return [ return Node(
Node(type: type, attributes: {ParagraphBlockKeys.delta: delta.toJson()}) type: type != ParagraphBlockKeys.type ? type : BulletedListBlockKeys.type,
]; attributes: {ParagraphBlockKeys.delta: delta.toJson()},
);
} }
Node _parseParagraphElement(dom.Element element) { Node _parseParagraphElement(dom.Element element) {
@ -278,6 +288,7 @@ class HTMLTags {
static const underline = 'u'; static const underline = 'u';
static const del = 'del'; static const del = 'del';
static const strong = 'strong'; static const strong = 'strong';
static const checkbox = 'input';
static const span = 'span'; static const span = 'span';
static const code = 'code'; static const code = 'code';
static const blockQuote = 'blockquote'; static const blockQuote = 'blockquote';
@ -302,15 +313,19 @@ class HTMLTags {
HTMLTags.h3, HTMLTags.h3,
HTMLTags.unorderedList, HTMLTags.unorderedList,
HTMLTags.orderedList, HTMLTags.orderedList,
HTMLTag.div,
HTMLTags.list, HTMLTags.list,
HTMLTags.paragraph, HTMLTags.paragraph,
HTMLTags.blockQuote, HTMLTags.blockQuote,
HTMLTags.checkbox,
HTMLTag.image
]; ];
static bool isTopLevel(String tag) { static bool isTopLevel(String tag) {
return tag == h1 || return tag == h1 ||
tag == h2 || tag == h2 ||
tag == h3 || tag == h3 ||
tag == checkbox ||
tag == paragraph || tag == paragraph ||
tag == div || tag == div ||
tag == blockQuote; tag == blockQuote;

View file

@ -1,13 +1,239 @@
import 'dart:convert'; import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/core/document/document.dart'; import 'package:html/dom.dart' as dom;
import 'package:flutter/material.dart';
class DocumentHTMLEncoder extends Converter<Document, String> { class DocumentHTMLEncoder extends Converter<Document, String> {
DocumentHTMLEncoder(); DocumentHTMLEncoder();
dom.Element? _stashListContainer;
final List<dom.Node> _result = [];
final List<Node> nodes = [];
@override @override
String convert(Document input) { String convert(Document input) {
final buffer = StringBuffer(); List<Node> documentNodes = input.root.children.toList();
return buffer.toString(); nodes.addAll(documentNodes);
return toHTMLString();
}
List<dom.Node> toHTMLNodes() {
for (final documentNode in nodes) {
if (documentNode.type != ImageBlockKeys.type) {
_addTextNode(documentNode);
} else {
final anchor = dom.Element.tag(HTMLTags.image);
anchor.attributes["src"] = documentNode.attributes[ImageBlockKeys.url];
_result.add(anchor);
}
}
if (_stashListContainer != null) {
_result.add(_stashListContainer!);
_stashListContainer = null;
}
return _result;
}
void _addTextNode(
Node documentNode,
) {
_addElement(
documentNode,
_textNodeToHtml(
documentNode,
),
);
}
void _addElement(Node documentNode, dom.Element element) {
if (element.localName == HTMLTags.list) {
final isNumbered = documentNode.type == NumberedListBlockKeys.type;
_stashListContainer ??= dom.Element.tag(
isNumbered ? HTMLTags.orderedList : HTMLTags.unorderedList,
);
_stashListContainer?.append(element);
} else {
if (_stashListContainer != null) {
_result.add(_stashListContainer!);
_stashListContainer = null;
}
_result.add(element);
}
}
String toHTMLString() {
final elements = toHTMLNodes();
final copyString = elements.fold<String>(
'',
(previousValue, element) => previousValue + stringify(element),
);
return copyString.replaceAll("\n", "");
}
dom.Element _textNodeToHtml(
Node documentNode,
) {
return _deltaToHtml(
Delta.fromJson(documentNode.attributes[ParagraphBlockKeys.delta]),
type: documentNode.type,
attributes: documentNode.attributes,
);
}
String _textDecorationsFromAttributes(Attributes attributes) {
final List<String> textDecoration = [];
if (attributes[BuiltInAttributeKey.strikethrough] == true) {
textDecoration.add('line-through');
}
if (attributes[BuiltInAttributeKey.underline] == true) {
textDecoration.add('underline');
}
return textDecoration.join(' ');
}
String _attributesToCssStyle(Map<String, dynamic> attributes) {
final cssMap = <String, String>{};
if (attributes[BuiltInAttributeKey.highlightColor] != null) {
final color = Color(
int.tryParse(attributes[BuiltInAttributeKey.highlightColor]) ??
0xFFFFFFFF,
);
cssMap['background-color'] = color.toRgbaString();
}
if (attributes[BuiltInAttributeKey.textColor] != null) {
final color = Color(
int.parse(attributes[BuiltInAttributeKey.textColor]),
);
cssMap['color'] = color.toRgbaString();
}
if (attributes[BuiltInAttributeKey.bold] == true) {
cssMap['font-weight'] = 'bold';
}
final textDecoration = _textDecorationsFromAttributes(attributes);
if (textDecoration.isNotEmpty) {
cssMap['text-decoration'] = textDecoration;
}
if (attributes[BuiltInAttributeKey.italic] == true) {
cssMap['font-style'] = 'italic';
}
return _cssMapToCssStyle(cssMap);
}
String _cssMapToCssStyle(Map<String, String> cssMap) {
return cssMap.entries.map((e) => '${e.key}: ${e.value}').join('; ');
}
dom.Element _deltaToHtml(
Delta delta, {
required String type,
required Attributes attributes,
}) {
final childNodes = <dom.Node>[];
String tagName = HTMLTags.paragraph;
if (type == BulletedListBlockKeys.type ||
type == NumberedListBlockKeys.type) {
tagName = HTMLTags.list;
} else if (type == TodoListBlockKeys.type) {
final node = dom.Element.html('<input type="checkbox" />');
node.attributes['checked'] =
attributes[TodoListBlockKeys.checked].toString();
tagName = HTMLTags.checkbox;
childNodes.add(node);
} else if (type == HeadingBlockKeys.type) {
if (attributes[HeadingBlockKeys.level] == 1) {
tagName = HTMLTags.h1;
} else if (attributes[HeadingBlockKeys.level] == 2) {
tagName = HTMLTags.h2;
} else if (attributes[HeadingBlockKeys.level] == 3) {
tagName = HTMLTags.h3;
}
} else if (type == QuoteBlockKeys.type) {
tagName = HTMLTags.blockQuote;
}
for (final op in delta) {
if (op is TextInsert) {
final attributes = op.attributes;
if (attributes != null) {
if (attributes.length == 1) {
final element = _applyAttributes(attributes, text: op.text);
childNodes.add(element);
} else {
final span = dom.Element.tag(HTMLTags.span);
final cssString = _attributesToCssStyle(attributes);
if (cssString.isNotEmpty) {
span.attributes['style'] = cssString;
}
span.append(dom.Text(op.text));
childNodes.add(span);
}
} else {
childNodes.add(dom.Text(op.text));
}
}
}
if (tagName == HTMLTags.blockQuote) {
final blockQuote = dom.Element.tag(tagName);
blockQuote.append(_insertText(HTMLTag.paragraph, childNodes: childNodes));
return blockQuote;
} else if (tagName == HTMLTags.checkbox) {
return _insertText(HTMLTag.div, childNodes: childNodes);
} else if (!HTMLTags.isTopLevel(tagName)) {
final result = dom.Element.tag(HTMLTags.list);
result.append(_insertText(HTMLTag.paragraph, childNodes: childNodes));
return result;
} else {
return _insertText(tagName, childNodes: childNodes);
}
}
dom.Element _applyAttributes(Attributes attributes, {required String text}) {
if (attributes[FlowyRichTextKeys.bold] == true) {
final strong = dom.Element.tag(HTMLTags.strong);
strong.append(dom.Text(text));
return strong;
} else if (attributes[FlowyRichTextKeys.underline] == true) {
final underline = dom.Element.tag(HTMLTags.underline);
underline.append(dom.Text(text));
return underline;
} else if (attributes[FlowyRichTextKeys.italic] == true) {
final italic = dom.Element.tag(HTMLTags.italic);
italic.append(dom.Text(text));
return italic;
} else if (attributes[FlowyRichTextKeys.strikethrough] == true) {
final del = dom.Element.tag(HTMLTags.del);
del.append(dom.Text(text));
return del;
} else if (attributes[FlowyRichTextKeys.code] == true) {
final code = dom.Element.tag(HTMLTags.code);
code.append(dom.Text(text));
return code;
} else if (attributes[FlowyRichTextKeys.href] != null) {
final anchor = dom.Element.tag(HTMLTags.anchor);
anchor.attributes['href'] = attributes[FlowyRichTextKeys.href];
anchor.append(dom.Text(text));
return anchor;
} else {
final paragraph = dom.Element.tag(HTMLTags.paragraph);
paragraph.append(dom.Text(text));
return paragraph;
}
}
dom.Element _insertText(
String tagName, {
required List<dom.Node> childNodes,
}) {
final p = dom.Element.tag(tagName);
for (final node in childNodes) {
p.append(node);
}
return p;
} }
} }

View file

@ -4,341 +4,288 @@ import 'package:appflowy_editor/appflowy_editor.dart';
void main() async { void main() async {
group('document_html_decoder_test.dart', () { group('document_html_decoder_test.dart', () {
const example = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'AppFlowyEditor'}
]
}
},
{
'type': 'heading',
'data': {
'level': 2,
'delta': [
{'insert': '👋 '},
{
'insert': 'Welcome to',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'AppFlowy Editor',
'attributes': {
'bold': true,
'italic': true,
'href': 'appflowy.io'
}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'AppFlowy Editor is a '},
{
'insert': 'highly customizable',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'rich-text editor',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert': 'Here',
'attributes': {'underline': true}
},
{'insert': ' is an example '},
{
'insert': 'your',
'attributes': {'strikethrough': true}
},
{'insert': ' you can give a try'}
]
}
},
{
'type': 'heading',
'data': {
'level': 3,
'delta': [
{'insert': 'Features!'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': '[x] Customizable'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': '[x] Test-covered'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': '[ ] more to come!'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': 'First item'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': 'Second item'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'List element'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert': '\n'
' This is a quote!\n'
''
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'You can also use '},
{
'insert': 'AppFlowy Editor',
'attributes': {'bold': true, 'italic': true}
},
{'insert': ' as a component to build your own app.'}
]
}
},
{
'type': 'heading',
'data': {
'level': 3,
'delta': [
{'insert': 'Awesome features'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!'
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert': '\n'
'\n'
' \n'
'\n'
'\n'
'\n'
'\n'
'\n'
''
},
{
'insert': 'Span element',
'attributes': {'bold': true, 'italic': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Span element two',
'attributes': {'underline': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Span element three',
'attributes': {'bold': true, 'strikethrough': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'This is an anchor tag!',
'attributes': {'href': 'https://appflowy.io'}
},
{
'insert': '\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
''
},
{
'insert': '\n'
' Code block\n'
'',
'attributes': {'code': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Italic one',
'attributes': {'italic': true}
},
{'insert': ' '},
{
'insert': 'Italic two',
'attributes': {'italic': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Bold tag',
'attributes': {'bold': true}
},
{
'insert': '\n'
'\n'
'\n'
'\n'
'\n'
''
}
]
}
}
]
}
};
setUpAll(() { setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
}); });
test('parser document', () async { test('parser document', () async {
const rawHTML = """<h1>AppFlowyEditor</h1>
<h2>👋 <strong>Welcome to</strong> <strong><em><a href="appflowy.io">AppFlowy Editor</a></em></strong></h2>
<p>AppFlowy Editor is a <strong>highly customizable</strong> <em>rich-text editor</em></p>
<hr />
<p><u>Here</u> is an example <del>your</del> you can give a try</p>
<span style="font-weight: bold;background-color: #cccccc;font-style: italic;">Span element</span>
<span style="font-weight: medium;text-decoration: underline;">Span element two</span>
<span style="font-weight: 900;text-decoration: line-through;">Span element three</span>
<a href="https://appflowy.io">This is an anchor tag!</a>
<img src="https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=1500w" />
<h3>Features!</h3>
<ul>
<li>[x] Customizable</li>
<li>[x] Test-covered</li>
<li>[ ] more to come!</li>
</ul>
<ol>
<li>First item</li>
<li>Second item</li>
</ol>
<li>List element</li>
<blockquote>
<p>This is a quote!</p>
</blockquote>
<code>
Code block
</code>
<em>Italic one</em> <i>Italic two</i>
<b>Bold tag</b>
<img src="http://appflowy.io" alt="AppFlowy">
<p>You can also use <strong><em>AppFlowy Editor</em></strong> as a component to build your own app.</p>
<h3>Awesome features</h3>
<p>If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!</p>
<hr>""";
final result = DocumentHTMLDecoder().convert(rawHTML); final result = DocumentHTMLDecoder().convert(rawHTML);
expect(result.toJson(), example); expect(result.toJson(), example);
}); });
}); });
} }
const rawHTML =
'''<h1>AppFlowyEditor</h1><h2>👋 <strong>Welcome to</strong> <span style="font-weight: bold; font-style: italic">AppFlowy Editor</span></h2><p>AppFlowy Editor is a <strong>highly customizable</strong> <i>rich-text editor</i></p><p> <u>Here</u> is an example <del>your</del> you can give a try</p><p> <span style="font-weight: bold; font-style: italic">Span element</span></p><p> <u>Span element two</u></p><p> <span style="font-weight: bold; text-decoration: line-through">Span element three</span></p><p> <a href="https://appflowy.io">This is an anchor tag!</a></p><h3>Features!</h3><ul><li><p>[x] Customizable</p></li><li><p>[x] Test-covered</p></li><li><p>[ ] more to come!</p></li><li><p>First item</p></li><li><p>Second item</p></li><li><p>List element</p></li></ul><blockquote><p>This is a quote!</p></blockquote><p><code> Code block</code></p><p> <i>Italic one</i></p><p> <i>Italic two</i></p><p> <strong>Bold tag</strong></p><p>You can also use <span style="font-weight: bold; font-style: italic">AppFlowy Editor</span> as a component to build your own app. </p><h3>Awesome features</h3><p>If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!</p><p></p><p></p>''';
const example = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'AppFlowyEditor'}
]
}
},
{
'type': 'heading',
'data': {
'level': 2,
'delta': [
{'insert': '👋 '},
{
'insert': 'Welcome to',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'AppFlowy Editor',
'attributes': {'bold': true, 'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'AppFlowy Editor is a '},
{
'insert': 'highly customizable',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'rich-text editor',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Here',
'attributes': {'underline': true}
},
{'insert': ' is an example '},
{
'insert': 'your',
'attributes': {'strikethrough': true}
},
{'insert': ' you can give a try'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Span element',
'attributes': {'bold': true, 'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Span element two',
'attributes': {'underline': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Span element three',
'attributes': {'bold': true, 'strikethrough': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'This is an anchor tag!',
'attributes': {'href': 'https://appflowy.io'}
}
]
}
},
{
'type': 'heading',
'data': {
'level': 3,
'delta': [
{'insert': 'Features!'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': '[x] Customizable'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': '[x] Test-covered'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': '[ ] more to come!'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'First item'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Second item'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'List element'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{'insert': 'This is a quote!'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert': ' Code block',
'attributes': {'code': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Italic one',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Italic two',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Bold tag',
'attributes': {'bold': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'You can also use '},
{
'insert': 'AppFlowy Editor',
'attributes': {'bold': true, 'italic': true}
},
{'insert': ' as a component to build your own app. '}
]
}
},
{
'type': 'heading',
'data': {
'level': 3,
'delta': [
{'insert': 'Awesome features'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
},
{
'type': 'paragraph',
'data': {'delta': []}
}
]
}
};

View file

@ -0,0 +1,290 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
void main() async {
group('document_html_encoder_test.dart', () {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
test('parser document', () async {
final result = DocumentHTMLEncoder().convert(Document.fromJson(delta));
expect(result, example);
});
});
}
const example =
'''<h1>AppFlowyEditor</h1><h2>👋 <strong>Welcome to</strong> <span style="font-weight: bold; font-style: italic">AppFlowy Editor</span></h2><p>AppFlowy Editor is a <strong>highly customizable</strong> <i>rich-text editor</i></p><p> <u>Here</u> is an example <del>your</del> you can give a try</p><p> <span style="font-weight: bold; font-style: italic">Span element</span></p><p> <u>Span element two</u></p><p> <span style="font-weight: bold; text-decoration: line-through">Span element three</span></p><p> <a href="https://appflowy.io">This is an anchor tag!</a></p><h3>Features!</h3><ul><li><p>[x] Customizable</p></li><li><p>[x] Test-covered</p></li><li><p>[ ] more to come!</p></li><li><p>First item</p></li><li><p>Second item</p></li><li><p>List element</p></li></ul><blockquote><p>This is a quote!</p></blockquote><p><code> Code block</code></p><p> <i>Italic one</i></p><p> <i>Italic two</i></p><p> <strong>Bold tag</strong></p><p>You can also use <span style="font-weight: bold; font-style: italic">AppFlowy Editor</span> as a component to build your own app. </p><h3>Awesome features</h3><p>If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!</p><p></p><p></p>''';
const delta = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'AppFlowyEditor'}
]
}
},
{
'type': 'heading',
'data': {
'level': 2,
'delta': [
{'insert': '👋 '},
{
'insert': 'Welcome to',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'AppFlowy Editor',
'attributes': {'bold': true, 'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'AppFlowy Editor is a '},
{
'insert': 'highly customizable',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'rich-text editor',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Here',
'attributes': {'underline': true}
},
{'insert': ' is an example '},
{
'insert': 'your',
'attributes': {'strikethrough': true}
},
{'insert': ' you can give a try'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Span element',
'attributes': {'bold': true, 'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Span element two',
'attributes': {'underline': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Span element three',
'attributes': {'bold': true, 'strikethrough': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'This is an anchor tag!',
'attributes': {'href': 'https://appflowy.io'}
}
]
}
},
{
'type': 'heading',
'data': {
'level': 3,
'delta': [
{'insert': 'Features!'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': '[x] Customizable'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': '[x] Test-covered'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': '[ ] more to come!'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'First item'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Second item'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'List element'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{'insert': 'This is a quote!'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert': ' Code block',
'attributes': {'code': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Italic one',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Italic two',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Bold tag',
'attributes': {'bold': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'You can also use '},
{
'insert': 'AppFlowy Editor',
'attributes': {'bold': true, 'italic': true}
},
{'insert': ' as a component to build your own app. '}
]
}
},
{
'type': 'heading',
'data': {
'level': 3,
'delta': [
{'insert': 'Awesome features'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
},
{
'type': 'paragraph',
'data': {'delta': []}
}
]
}
};

View file

@ -8,6 +8,12 @@ void main() {
expect(document.toJson(), data); expect(document.toJson(), data);
}); });
}); });
group('document_html_test.dart tests', () {
test('documentToHtml()', () {
final document = documentToHTML(Document.fromJson(data));
expect(document, rawHTML);
});
});
} }
const data = { const data = {
@ -33,14 +39,10 @@ const data = {
'insert': 'Welcome to', 'insert': 'Welcome to',
'attributes': {'bold': true} 'attributes': {'bold': true}
}, },
{'insert': ' '}, {'insert': ' '},
{ {
'insert': 'AppFlowy Editor', 'insert': 'AppFlowy Editor',
'attributes': { 'attributes': {'bold': true, 'italic': true}
'bold': true,
'italic': true,
'href': 'appflowy.io'
}
} }
] ]
} }
@ -54,7 +56,7 @@ const data = {
'insert': 'highly customizable', 'insert': 'highly customizable',
'attributes': {'bold': true} 'attributes': {'bold': true}
}, },
{'insert': ' '}, {'insert': ' '},
{ {
'insert': 'rich-text editor', 'insert': 'rich-text editor',
'attributes': {'italic': true} 'attributes': {'italic': true}
@ -66,6 +68,7 @@ const data = {
'type': 'paragraph', 'type': 'paragraph',
'data': { 'data': {
'delta': [ 'delta': [
{'insert': ' '},
{ {
'insert': 'Here', 'insert': 'Here',
'attributes': {'underline': true} 'attributes': {'underline': true}
@ -79,6 +82,54 @@ const data = {
] ]
} }
}, },
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Span element',
'attributes': {'bold': true, 'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Span element two',
'attributes': {'underline': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Span element three',
'attributes': {'bold': true, 'strikethrough': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'This is an anchor tag!',
'attributes': {'href': 'https://appflowy.io'}
}
]
}
},
{ {
'type': 'heading', 'type': 'heading',
'data': { 'data': {
@ -89,7 +140,7 @@ const data = {
} }
}, },
{ {
'type': 'numbered_list', 'type': 'bulleted_list',
'data': { 'data': {
'delta': [ 'delta': [
{'insert': '[x] Customizable'} {'insert': '[x] Customizable'}
@ -97,7 +148,7 @@ const data = {
} }
}, },
{ {
'type': 'numbered_list', 'type': 'bulleted_list',
'data': { 'data': {
'delta': [ 'delta': [
{'insert': '[x] Test-covered'} {'insert': '[x] Test-covered'}
@ -105,7 +156,7 @@ const data = {
} }
}, },
{ {
'type': 'numbered_list', 'type': 'bulleted_list',
'data': { 'data': {
'delta': [ 'delta': [
{'insert': '[ ] more to come!'} {'insert': '[ ] more to come!'}
@ -113,7 +164,7 @@ const data = {
} }
}, },
{ {
'type': 'numbered_list', 'type': 'bulleted_list',
'data': { 'data': {
'delta': [ 'delta': [
{'insert': 'First item'} {'insert': 'First item'}
@ -121,7 +172,7 @@ const data = {
} }
}, },
{ {
'type': 'numbered_list', 'type': 'bulleted_list',
'data': { 'data': {
'delta': [ 'delta': [
{'insert': 'Second item'} {'insert': 'Second item'}
@ -129,7 +180,7 @@ const data = {
} }
}, },
{ {
'type': 'paragraph', 'type': 'bulleted_list',
'data': { 'data': {
'delta': [ 'delta': [
{'insert': 'List element'} {'insert': 'List element'}
@ -138,12 +189,55 @@ const data = {
}, },
{ {
'type': 'quote', 'type': 'quote',
'data': {
'delta': [
{'insert': 'This is a quote!'}
]
}
},
{
'type': 'paragraph',
'data': { 'data': {
'delta': [ 'delta': [
{ {
'insert': '\n' 'insert': ' Code block',
' This is a quote!\n' 'attributes': {'code': true}
'' }
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Italic one',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Italic two',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': ' '},
{
'insert': 'Bold tag',
'attributes': {'bold': true}
} }
] ]
} }
@ -157,7 +251,7 @@ const data = {
'insert': 'AppFlowy Editor', 'insert': 'AppFlowy Editor',
'attributes': {'bold': true, 'italic': true} 'attributes': {'bold': true, 'italic': true}
}, },
{'insert': ' as a component to build your own app.'} {'insert': ' as a component to build your own app. '}
] ]
} }
}, },
@ -183,156 +277,14 @@ const data = {
}, },
{ {
'type': 'paragraph', 'type': 'paragraph',
'data': { 'data': {'delta': []}
'delta': [ },
{ {
'insert': '\n' 'type': 'paragraph',
'\n' 'data': {'delta': []}
' \n'
'\n'
'\n'
'\n'
'\n'
'\n'
''
},
{
'insert': 'Span element',
'attributes': {'bold': true, 'italic': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Span element two',
'attributes': {'underline': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Span element three',
'attributes': {'bold': true, 'strikethrough': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'This is an anchor tag!',
'attributes': {'href': 'https://appflowy.io'}
},
{
'insert': '\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
''
},
{
'insert': '\n'
' Code block\n'
'',
'attributes': {'code': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Italic one',
'attributes': {'italic': true}
},
{'insert': ' '},
{
'insert': 'Italic two',
'attributes': {'italic': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Bold tag',
'attributes': {'bold': true}
},
{
'insert': '\n'
'\n'
'\n'
'\n'
'\n'
''
}
]
}
} }
] ]
} }
}; };
const rawHTML = """<h1>AppFlowyEditor</h1> const rawHTML =
<h2>👋 <strong>Welcome to</strong> <strong><em><a href="appflowy.io">AppFlowy Editor</a></em></strong></h2> '''<h1>AppFlowyEditor</h1><h2>👋 <strong>Welcome to</strong> <span style="font-weight: bold; font-style: italic">AppFlowy Editor</span></h2><p>AppFlowy Editor is a <strong>highly customizable</strong> <i>rich-text editor</i></p><p> <u>Here</u> is an example <del>your</del> you can give a try</p><p> <span style="font-weight: bold; font-style: italic">Span element</span></p><p> <u>Span element two</u></p><p> <span style="font-weight: bold; text-decoration: line-through">Span element three</span></p><p> <a href="https://appflowy.io">This is an anchor tag!</a></p><h3>Features!</h3><ul><li><p>[x] Customizable</p></li><li><p>[x] Test-covered</p></li><li><p>[ ] more to come!</p></li><li><p>First item</p></li><li><p>Second item</p></li><li><p>List element</p></li></ul><blockquote><p>This is a quote!</p></blockquote><p><code> Code block</code></p><p> <i>Italic one</i></p><p> <i>Italic two</i></p><p> <strong>Bold tag</strong></p><p>You can also use <span style="font-weight: bold; font-style: italic">AppFlowy Editor</span> as a component to build your own app. </p><h3>Awesome features</h3><p>If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!</p><p></p><p></p>''';
<p>AppFlowy Editor is a <strong>highly customizable</strong> <em>rich-text editor</em></p>
<hr />
<p><u>Here</u> is an example <del>your</del> you can give a try</p>
<span style="font-weight: bold;background-color: #cccccc;font-style: italic;">Span element</span>
<span style="font-weight: medium;text-decoration: underline;">Span element two</span>
<span style="font-weight: 900;text-decoration: line-through;">Span element three</span>
<a href="https://appflowy.io">This is an anchor tag!</a>
<img src="https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=1500w" />
<h3>Features!</h3>
<ul>
<li>[x] Customizable</li>
<li>[x] Test-covered</li>
<li>[ ] more to come!</li>
</ul>
<ol>
<li>First item</li>
<li>Second item</li>
</ol>
<li>List element</li>
<blockquote>
<p>This is a quote!</p>
</blockquote>
<code>
Code block
</code>
<em>Italic one</em> <i>Italic two</i>
<b>Bold tag</b>
<img src="http://appflowy.io" alt="AppFlowy">
<p>You can also use <strong><em>AppFlowy Editor</em></strong> as a component to build your own app.</p>
<h3>Awesome features</h3>
<p>If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!</p>
<hr>""";