fix: copy handler migrated to latest api (#192)

* fix: copy handler migrated to latest api

* flutter analyze

* code formatted

* fix: test added for copy handler

* more test added for copy handler

* fix:write test for copy handler

* text added for html to nodes

* fix: added more test for html encoder

* fix:removed unused imports

* fix: copyhandler collapse state test added

* added direct event handler in test

* flutter analyze

* revert changes in example.json
This commit is contained in:
Ali Hassan 2023-06-20 14:12:14 +05:00 • committed by GitHub
commit ad838cfa86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 2829 additions and 879 deletions

View file

@ -50,7 +50,6 @@ export 'src/core/document/node_iterator.dart';
export 'src/infra/flowy_svg.dart';
export 'src/extensions/extensions.dart';
export 'src/service/default_text_operations/format_rich_text_style.dart';
export 'src/infra/html_converter.dart';
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
export 'src/editor/block_component/block_component.dart';
@ -63,8 +62,11 @@ export 'src/service/standard_block_components.dart';
export 'src/core/document/deprecated/node.dart';
export 'src/core/document/deprecated/document.dart';
export 'src/plugins/html/html_document_decoder.dart';
export 'src/plugins/html/html_document_encoder.dart';
export 'src/plugins/html/decoder/html_document_decoder.dart';
export 'src/plugins/html/encoder/html_document_encoder.dart';
export 'src/plugins/html/decoder/html_nodes_decoder.dart';
export 'src/plugins/html/encoder/html_nodes_encoder.dart';
export 'src/plugins/html/html_document.dart';
export 'src/plugins/html/html_nodes.dart';
export 'src/infra/mobile/mobile.dart';
export 'src/infra/clipboard.dart';

View file

@ -1,5 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/clipboard.dart';
import 'package:flutter/material.dart';
/// Copy.
@ -32,11 +31,7 @@ CommandShortcutEventHandler _copyCommandHandler = (editorState) {
// rich text.
// TODO: support rich text. the below code is not working.
final nodes = editorState.getNodesInSelection(selection);
final html = NodesToHTMLConverter(
nodes: nodes,
startOffset: selection.startIndex,
endOffset: selection.endIndex,
).toHTMLString();
final html = nodesToHTML(nodes);
AppFlowyClipboard.setData(
text: text,

View file

@ -1,5 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/clipboard.dart';
import 'package:flutter/material.dart';
/// Paste.

View file

@ -1,6 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/toolbar/desktop/items/link/link_menu.dart';
import 'package:appflowy_editor/src/infra/clipboard.dart';
import 'package:flutter/material.dart';
final linkItem = ToolbarItem(

View file

@ -1,685 +0,0 @@
import 'dart:collection';
import 'dart:ui';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:html/parser.dart' show parse;
import 'package:html/dom.dart' as html;
class HTMLTag {
static const h1 = 'h1';
static const h2 = 'h2';
static const h3 = 'h3';
static const orderedList = 'ol';
static const unorderedList = 'ul';
static const list = 'li';
static const paragraph = 'p';
static const image = 'img';
static const anchor = 'a';
static const italic = 'i';
static const em = 'em';
static const bold = 'b';
static const underline = 'u';
static const del = 'del';
static const strong = 'strong';
static const span = 'span';
static const code = 'code';
static const blockQuote = 'blockquote';
static const div = 'div';
static const divider = 'hr';
static bool isTopLevel(String tag) {
return tag == h1 ||
tag == h2 ||
tag == h3 ||
tag == paragraph ||
tag == div ||
tag == blockQuote;
}
}
/// Converting the HTML to nodes
class HTMLToNodesConverter {
final html.Document _document;
/// This flag is used for parsing HTML pasting from Google Docs
/// Google docs wraps the the content inside the `<b></b>` tag. It's strange.
///
/// If a `<b>` element is parsing in the <p>, we regard it as as text spans.
/// Otherwise, it's parsed as a container.
bool _inParagraph = false;
HTMLToNodesConverter(String htmlString) : _document = parse(htmlString);
List<Node> toNodes() {
final childNodes = _document.body?.nodes.toList() ?? <html.Node>[];
return _handleContainer(childNodes);
}
Document toDocument() {
final childNodes = _document.body?.nodes.toList() ?? <html.Node>[];
return Document.fromJson({
'document': {
'type': 'page',
'children': _handleContainer(childNodes).map((e) => e.toJson()).toList()
}
});
}
List<Node> _handleContainer(List<html.Node> childNodes) {
final delta = Delta();
final result = <Node>[];
for (final child in childNodes) {
if (child is html.Element) {
if (child.localName == HTMLTag.anchor ||
child.localName == HTMLTag.span ||
child.localName == HTMLTag.code ||
child.localName == HTMLTag.strong ||
child.localName == HTMLTag.underline ||
child.localName == HTMLTag.italic ||
child.localName == HTMLTag.em ||
child.localName == HTMLTag.del) {
_handleRichTextElement(delta, child);
} else if (child.localName == HTMLTag.bold) {
// Google docs wraps the the content inside the `<b></b>` tag.
// It's strange
if (!_inParagraph) {
result.addAll(_handleBTag(child));
} else {
result.add(_handleRichText(child));
}
} else if (child.localName == HTMLTag.blockQuote) {
result.addAll(_handleBlockQuote(child));
} else {
result.addAll(_handleElement(child));
}
} else {
delta.insert(child.text ?? '');
}
}
if (delta.isNotEmpty) {
result.add(paragraphNode(delta: delta));
}
return result;
}
List<Node> _handleBlockQuote(html.Element element) {
final result = <Node>[];
for (final child in element.nodes.toList()) {
if (child is html.Element) {
result.addAll(
_handleElement(child, {'subtype': BuiltInAttributeKey.quote}),
);
}
}
return result;
}
List<Node> _handleBTag(html.Element element) {
final childNodes = element.nodes;
return _handleContainer(childNodes);
}
List<Node> _handleElement(
html.Element element, [
Map<String, dynamic>? attributes,
]) {
if (element.localName == HTMLTag.h1) {
return [_handleHeadingElement(element, 1)];
} else if (element.localName == HTMLTag.h2) {
return [_handleHeadingElement(element, 2)];
} else if (element.localName == HTMLTag.h3) {
return [_handleHeadingElement(element, 3)];
} else if (element.localName == HTMLTag.unorderedList) {
return _handleUnorderedList(element);
} else if (element.localName == HTMLTag.orderedList) {
return _handleOrderedList(element);
} else if (element.localName == HTMLTag.list) {
return _handleListElement(element);
} else if (element.localName == HTMLTag.paragraph) {
return [_handleParagraph(element, attributes)];
} else if (element.localName == HTMLTag.image) {
return [_handleImage(element)];
} else if (element.localName == HTMLTag.divider) {
return [_handleDivider()];
} else {
return [paragraphNode(delta: Delta()..insert(element.text))];
}
}
Node _handleParagraph(
html.Element element, [
Map<String, dynamic>? attributes,
]) {
_inParagraph = true;
final node = _handleRichText(element, attributes);
_inParagraph = false;
return node;
}
Node _handleDivider() => paragraphNode(text: '---');
Map<String, String> _cssStringToMap(String? cssString) {
final result = <String, String>{};
if (cssString == null) {
return result;
}
final entries = cssString.split(';');
for (final entry in entries) {
final tuples = entry.split(':');
if (tuples.length < 2) {
continue;
}
result[tuples[0].trim()] = tuples[1].trim();
}
return result;
}
Attributes? _getDeltaAttributesFromHtmlAttributes(
LinkedHashMap<Object, String> htmlAttributes,
) {
final attrs = <String, dynamic>{};
final styleString = htmlAttributes['style'];
final cssMap = _cssStringToMap(styleString);
final fontWeightStr = cssMap['font-weight'];
if (fontWeightStr != null) {
if (fontWeightStr == 'bold') {
attrs[BuiltInAttributeKey.bold] = true;
} else {
int? weight = int.tryParse(fontWeightStr);
if (weight != null && weight > 500) {
attrs[BuiltInAttributeKey.bold] = true;
}
}
}
final textDecorationStr = cssMap['text-decoration'];
if (textDecorationStr != null) {
_assignTextDecorations(attrs, textDecorationStr);
}
final backgroundColorStr = cssMap['background-color'];
final backgroundColor = backgroundColorStr == null
? null
: ColorExtension2.tryFromRgbaString(backgroundColorStr);
if (backgroundColor != null) {
attrs[BuiltInAttributeKey.highlightColor] = backgroundColor.toHex();
}
if (cssMap['font-style'] == 'italic') {
attrs[BuiltInAttributeKey.italic] = true;
}
return attrs.isEmpty ? null : attrs;
}
void _assignTextDecorations(Attributes attrs, String decorationStr) {
final decorations = decorationStr.split(' ');
for (final d in decorations) {
if (d == 'line-through') {
attrs[BuiltInAttributeKey.strikethrough] = true;
} else if (d == 'underline') {
attrs[BuiltInAttributeKey.underline] = true;
}
}
}
void _handleRichTextElement(Delta delta, html.Element element) {
if (element.localName == HTMLTag.span) {
delta.insert(
element.text,
attributes: _getDeltaAttributesFromHtmlAttributes(element.attributes),
);
} else if (element.localName == HTMLTag.anchor) {
final hyperLink = element.attributes['href'];
Map<String, dynamic>? attributes;
if (hyperLink != null) {
attributes = {'href': hyperLink};
}
delta.insert(element.text, attributes: attributes);
} else if (element.localName == HTMLTag.strong ||
element.localName == HTMLTag.bold) {
delta.insert(element.text, attributes: {BuiltInAttributeKey.bold: true});
} else if ([HTMLTag.em, HTMLTag.italic].contains(element.localName)) {
delta.insert(
element.text,
attributes: {BuiltInAttributeKey.italic: true},
);
} else if (element.localName == HTMLTag.underline) {
delta.insert(
element.text,
attributes: {BuiltInAttributeKey.underline: true},
);
} else if ([HTMLTag.italic, HTMLTag.em].contains(element.localName)) {
delta
.insert(element.text, attributes: {BuiltInAttributeKey.italic: true});
} else if (element.localName == HTMLTag.del) {
delta.insert(
element.text,
attributes: {BuiltInAttributeKey.strikethrough: true},
);
} else if (element.localName == HTMLTag.code) {
delta.insert(element.text, attributes: {BuiltInAttributeKey.code: true});
} else {
delta.insert(element.text);
}
}
/// A container contains a <input type='checkbox' > will
/// be regarded as a checkbox block.
///
/// A container contains a <img /> will be regarded as a image block
Node _handleRichText(
html.Element element, [
Map<String, dynamic>? attributes,
]) {
final image = element.querySelector(HTMLTag.image);
if (image != null) {
final imageNode = _handleImage(image);
return imageNode;
}
final testInput = element.querySelector('input');
bool checked = false;
final isCheckbox =
testInput != null && testInput.attributes['type'] == 'checkbox';
if (isCheckbox) {
checked = testInput.attributes.containsKey('checked') &&
testInput.attributes['checked'] != 'false';
}
final delta = Delta();
for (final child in element.nodes.toList()) {
if (child is html.Element) {
_handleRichTextElement(delta, child);
} else {
delta.insert(child.text ?? '');
}
}
final subtype = attributes?['subtype'];
if (subtype != null) {
if (subtype == BuiltInAttributeKey.numberList) {
return numberedListNode(delta: delta);
} else if (subtype == BuiltInAttributeKey.bulletedList) {
return bulletedListNode(delta: delta);
} else if (isCheckbox) {
return todoListNode(delta: delta, checked: checked);
} else if (subtype == BuiltInAttributeKey.quote) {
return quoteNode(delta: delta);
} else {
return paragraphNode(delta: delta);
}
} else {
return paragraphNode(delta: delta);
}
}
Node _handleImage(html.Element element) {
final src = element.attributes['src'] ?? '';
return imageNode(
url: src,
);
}
List<Node> _handleUnorderedList(html.Element element) {
final result = <Node>[];
for (var child in element.children) {
result.addAll(
_handleListElement(
child,
{'subtype': BuiltInAttributeKey.bulletedList},
),
);
}
return result;
}
List<Node> _handleOrderedList(html.Element element) {
final result = <Node>[];
for (var i = 0; i < element.children.length; i++) {
final child = element.children[i];
result.addAll(
_handleListElement(
child,
{'subtype': BuiltInAttributeKey.numberList, 'number': i + 1},
),
);
}
return result;
}
Node _handleHeadingElement(
html.Element element,
int level,
) {
return headingNode(
level: level,
delta: Delta()..insert(element.text),
);
}
List<Node> _handleListElement(
html.Element element, [
Map<String, dynamic>? attributes,
]) {
final result = <Node>[];
final childNodes = element.nodes.toList();
for (final child in childNodes) {
if (child is html.Element) {
result.addAll(_handleElement(child, attributes));
}
}
return result;
}
}
/// [NodesToHTMLConverter] is used to convert the nodes to HTML.
/// Can be used to copy & paste, exporting the document.
class NodesToHTMLConverter {
final List<Node> nodes;
final int? startOffset;
final int? endOffset;
final List<html.Node> _result = [];
/// According to the W3C specs. The bullet list should be wrapped as
///
/// <ul>
/// <li>xxx</li>
/// <li>xxx</li>
/// <li>xxx</li>
/// </ul>
///
/// This container is used to save the list elements temporarily.
html.Element? _stashListContainer;
NodesToHTMLConverter({
required this.nodes,
this.startOffset,
this.endOffset,
}) {
if (nodes.isEmpty) {
return;
} else if (nodes.length == 1) {
final first = nodes.first;
if (first is TextNode) {
nodes[0] = first.copyWith(
delta: first.delta.slice(startOffset ?? 0, endOffset),
);
}
} else {
final first = nodes.first;
final last = nodes.last;
if (first is TextNode) {
nodes[0] = first.copyWith(delta: first.delta.slice(startOffset ?? 0));
}
if (last is TextNode) {
nodes[nodes.length - 1] =
last.copyWith(delta: last.delta.slice(0, endOffset));
}
}
}
List<html.Node> toHTMLNodes() {
for (final node in nodes) {
if (node.type == 'text') {
final textNode = node as TextNode;
if (node == nodes.first) {
_addTextNode(textNode);
} else if (node == nodes.last) {
_addTextNode(textNode, end: endOffset);
} else {
_addTextNode(textNode);
}
} else if (node.type == 'image') {
final textNode = node;
final anchor = html.Element.tag(HTMLTag.image);
anchor.attributes['src'] = textNode.attributes['image_src'];
_result.add(anchor);
}
// TODO: handle image and other blocks
}
if (_stashListContainer != null) {
_result.add(_stashListContainer!);
_stashListContainer = null;
}
return _result;
}
void _addTextNode(TextNode textNode, {int? end}) {
_addElement(textNode, _textNodeToHtml(textNode, end: end));
}
void _addElement(TextNode textNode, html.Element element) {
if (element.localName == HTMLTag.list) {
final isNumbered =
textNode.attributes['subtype'] == BuiltInAttributeKey.numberList;
_stashListContainer ??= html.Element.tag(
isNumbered ? HTMLTag.orderedList : HTMLTag.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;
}
html.Element _textNodeToHtml(TextNode textNode, {int? end}) {
String? subType = textNode.attributes['subtype'];
String? heading = textNode.attributes['heading'];
return _deltaToHtml(
textNode.delta,
subType: subType,
heading: heading,
end: end,
checked: textNode.attributes['checkbox'] == true,
);
}
String _textDecorationsFromAttributes(Attributes attributes) {
var textDecoration = <String>[];
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.parse(attributes[BuiltInAttributeKey.highlightColor]),
);
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.fold('', (previousValue, element) {
final kv = '${element.key}: ${element.value}';
if (previousValue.isEmpty) {
return kv;
}
return '$previousValue; $kv';
});
}
/// Convert the rich text to HTML
///
/// Use `<b>` for bold only.
/// Use `<i>` for italic only.
/// Use `<del>` for strikethrough only.
/// Use `<u>` for underline only.
///
/// If the text has multiple styles, use a `<span>`
/// to mix the styles.
///
/// A CSS style string is used to describe the styles.
/// The HTML will be:
///
/// ```html
/// <span style='...'>Text</span>
/// ```
html.Element _deltaToHtml(
Delta delta, {
String? subType,
String? heading,
int? end,
bool? checked,
}) {
if (end != null) {
delta = delta.slice(0, end);
}
final childNodes = <html.Node>[];
String tagName = HTMLTag.paragraph;
if (subType == BuiltInAttributeKey.bulletedList ||
subType == BuiltInAttributeKey.numberList) {
tagName = HTMLTag.list;
} else if (subType == BuiltInAttributeKey.checkbox) {
final node = html.Element.html('<input type="checkbox" />');
if (checked != null && checked) {
node.attributes['checked'] = 'true';
}
childNodes.add(node);
} else if (subType == BuiltInAttributeKey.heading) {
if (heading == BuiltInAttributeKey.h1) {
tagName = HTMLTag.h1;
} else if (heading == BuiltInAttributeKey.h2) {
tagName = HTMLTag.h2;
} else if (heading == BuiltInAttributeKey.h3) {
tagName = HTMLTag.h3;
}
} else if (subType == BuiltInAttributeKey.quote) {
tagName = HTMLTag.blockQuote;
}
for (final op in delta) {
if (op is TextInsert) {
final attributes = op.attributes;
if (attributes != null) {
if (attributes.length == 1 &&
attributes[BuiltInAttributeKey.bold] == true) {
final strong = html.Element.tag(HTMLTag.strong);
strong.append(html.Text(op.text));
childNodes.add(strong);
} else if (attributes.length == 1 &&
attributes[BuiltInAttributeKey.underline] == true) {
final strong = html.Element.tag(HTMLTag.underline);
strong.append(html.Text(op.text));
childNodes.add(strong);
} else if (attributes.length == 1 &&
attributes[BuiltInAttributeKey.italic] == true) {
final strong = html.Element.tag(HTMLTag.italic);
strong.append(html.Text(op.text));
childNodes.add(strong);
} else if (attributes.length == 1 &&
attributes[BuiltInAttributeKey.strikethrough] == true) {
final strong = html.Element.tag(HTMLTag.del);
strong.append(html.Text(op.text));
childNodes.add(strong);
} else if (attributes.length == 1 &&
attributes[BuiltInAttributeKey.code] == true) {
final code = html.Element.tag(HTMLTag.code);
code.append(html.Text(op.text));
childNodes.add(code);
} else if (attributes.length == 1 &&
attributes[BuiltInAttributeKey.href] != null) {
final anchor = html.Element.tag(HTMLTag.anchor);
anchor.attributes['href'] = attributes[BuiltInAttributeKey.href];
anchor.append(html.Text(op.text));
childNodes.add(anchor);
} else {
final span = html.Element.tag(HTMLTag.span);
final cssString = _attributesToCssStyle(attributes);
if (cssString.isNotEmpty) {
span.attributes['style'] = cssString;
}
span.append(html.Text(op.text));
childNodes.add(span);
}
} else {
childNodes.add(html.Text(op.text));
}
}
}
if (tagName == HTMLTag.blockQuote) {
final p = html.Element.tag(HTMLTag.paragraph);
for (final node in childNodes) {
p.append(node);
}
final blockQuote = html.Element.tag(tagName);
blockQuote.append(p);
return blockQuote;
} else if (!HTMLTag.isTopLevel(tagName)) {
final p = html.Element.tag(HTMLTag.paragraph);
for (final node in childNodes) {
p.append(node);
}
final result = html.Element.tag(HTMLTag.list);
result.append(p);
return result;
} else {
final p = html.Element.tag(tagName);
for (final node in childNodes) {
p.append(node);
}
return p;
}
}
}
String stringify(html.Node node) {
if (node is html.Element) {
return node.outerHtml;
}
if (node is html.Text) {
return node.text;
}
return '';
}

View file

@ -0,0 +1,16 @@
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
class DocumentHTMLDecoder extends Converter<String, Document> {
DocumentHTMLDecoder();
@override
Document convert(String input) {
final nodes = htmlToNodes(input);
return Document.blank(withInitialText: false)
..insert(
[0],
nodes,
);
}
}

View file

@ -0,0 +1,351 @@
import 'dart:collection';
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart';
class NodeHTMLDecoder extends Converter<String, List<Node>> {
NodeHTMLDecoder();
@override
List<Node> convert(String input) {
final document = parse(input);
final body = document.body;
if (body == null) {
return [];
}
final nodes = _parseElement(body.nodes);
return nodes.toList();
}
Iterable<Node> _parseElement(Iterable<dom.Node> domNodes) {
final delta = Delta();
final List<Node> nodes = [];
for (final domNode in domNodes) {
if (domNode is dom.Element) {
final localName = domNode.localName;
if (HTMLTags.formattingElements.contains(localName)) {
final attributes = _parserFormattingElementAttributes(domNode);
nodes.add(
paragraphNode(
delta: Delta()..insert(domNode.text, attributes: attributes),
),
);
} else if (HTMLTags.specialElements.contains(localName)) {
nodes.addAll(
_parseSpecialElements(
domNode,
type: ParagraphBlockKeys.type,
),
);
}
} else if (domNode is dom.Text) {
delta.insert(domNode.text);
} else {
assert(false, 'Unknown node type: $domNode');
}
}
if (delta.isNotEmpty) {
nodes.add(paragraphNode(delta: delta));
}
return nodes;
}
Iterable<Node> _parseSpecialElements(
dom.Element element, {
required String type,
}) {
final localName = element.localName;
switch (localName) {
case HTMLTags.h1:
return _parseHeadingElement(element, level: 1);
case HTMLTags.h2:
return _parseHeadingElement(element, level: 2);
case HTMLTags.h3:
return _parseHeadingElement(element, level: 3);
case HTMLTags.unorderedList:
return _parseUnOrderListElement(element);
case HTMLTags.orderedList:
return _parseOrderListElement(element);
case HTMLTags.list:
return [
_parseListElement(
element,
type: type,
)
];
case HTMLTags.paragraph:
return _parseParagraphElement(element);
case HTMLTags.blockQuote:
return [_parseBlockQuoteElement(element)];
case HTMLTags.image:
return [_parseImageElement(element)];
default:
return _parseParagraphElement(element);
}
}
Attributes _parserFormattingElementAttributes(
dom.Element element,
) {
final localName = element.localName;
Attributes attributes = {};
switch (localName) {
case HTMLTags.bold || HTMLTags.strong:
attributes = {FlowyRichTextKeys.bold: true};
break;
case HTMLTags.italic || HTMLTags.em:
attributes = {FlowyRichTextKeys.italic: true};
break;
case HTMLTags.underline:
attributes = {FlowyRichTextKeys.underline: true};
break;
case HTMLTags.del:
attributes = {FlowyRichTextKeys.strikethrough: true};
break;
case HTMLTags.code:
attributes = {FlowyRichTextKeys.code: true};
case HTMLTags.span:
final deltaAttributes = _getDeltaAttributesFromHTMLAttributes(
element.attributes,
) ??
{};
attributes.addAll(deltaAttributes);
break;
case HTMLTags.anchor:
final href = element.attributes['href'];
if (href != null) {
attributes = {FlowyRichTextKeys.href: href};
}
break;
default:
break;
}
for (final child in element.children) {
attributes.addAll(_parserFormattingElementAttributes(child));
}
return attributes;
}
Iterable<Node> _parseHeadingElement(
dom.Element element, {
required int level,
}) {
final (delta, specialNodes) = _parseDeltaElement(element);
return [
headingNode(
level: level,
delta: delta,
),
...specialNodes
];
}
Node _parseBlockQuoteElement(dom.Element element) {
final (delta, nodes) = _parseDeltaElement(element);
return quoteNode(
delta: delta,
children: nodes,
);
}
Iterable<Node> _parseUnOrderListElement(dom.Element element) {
return element.children
.map(
(child) => _parseListElement(child, type: BulletedListBlockKeys.type),
)
.toList();
}
Iterable<Node> _parseOrderListElement(dom.Element element) {
return element.children
.map(
(child) => _parseListElement(child, type: NumberedListBlockKeys.type),
)
.toList();
}
Node _parseListElement(
dom.Element element, {
required String type,
}) {
final (delta, node) = _parseDeltaElement(element);
return Node(
type: type != ParagraphBlockKeys.type ? type : BulletedListBlockKeys.type,
children: node,
attributes: {ParagraphBlockKeys.delta: delta.toJson()},
);
}
Iterable<Node> _parseParagraphElement(dom.Element element) {
final (delta, specialNodes) = _parseDeltaElement(element);
return [paragraphNode(delta: delta), ...specialNodes];
}
Node _parseImageElement(dom.Element element) {
final src = element.attributes['src'] ?? '';
return imageNode(
url: src,
);
}
(Delta, Iterable<Node>) _parseDeltaElement(dom.Element element) {
final delta = Delta();
final nodes = <Node>[];
final children = element.nodes.toList();
for (final child in children) {
if (child is dom.Element) {
if (child.children.isNotEmpty) {
for (final seocondChild in child.children) {
nodes.addAll(
_parseSpecialElements(
seocondChild,
type: ParagraphBlockKeys.type,
),
);
}
} else {
final attributes = _parserFormattingElementAttributes(child);
delta.insert(child.text, attributes: attributes);
}
} else {
delta.insert(child.text ?? '');
}
}
return (delta, nodes);
}
Attributes? _getDeltaAttributesFromHTMLAttributes(
LinkedHashMap<Object, String> htmlAttributes,
) {
final Attributes attributes = {};
final style = htmlAttributes['style'];
final css = _getCssFromString(style);
// font weight
final fontWeight = css['font-weight'];
if (fontWeight != null) {
if (fontWeight == 'bold') {
attributes[FlowyRichTextKeys.bold] = true;
} else {
final weight = int.tryParse(fontWeight);
if (weight != null && weight >= 500) {
attributes[FlowyRichTextKeys.bold] = true;
}
}
}
// decoration
final textDecoration = css['text-decoration'];
if (textDecoration != null) {
final decorations = textDecoration.split(' ');
for (final decoration in decorations) {
switch (decoration) {
case 'underline':
attributes[FlowyRichTextKeys.underline] = true;
break;
case 'line-through':
attributes[FlowyRichTextKeys.strikethrough] = true;
break;
default:
break;
}
}
}
// background color
final backgroundColor = css['background-color'];
if (backgroundColor != null) {
final highlightColor = backgroundColor.tryToColor()?.toHex();
if (highlightColor != null) {
attributes[FlowyRichTextKeys.highlightColor] = highlightColor;
}
}
// italic
final fontStyle = css['font-style'];
if (fontStyle == 'italic') {
attributes[FlowyRichTextKeys.italic] = true;
}
return attributes.isEmpty ? null : attributes;
}
Map<String, String> _getCssFromString(String? cssString) {
final Map<String, String> result = {};
if (cssString == null) {
return result;
}
final entries = cssString.split(';');
for (final entry in entries) {
final tuples = entry.split(':');
if (tuples.length < 2) {
continue;
}
result[tuples[0].trim()] = tuples[1].trim();
}
return result;
}
}
class HTMLTags {
static const h1 = 'h1';
static const h2 = 'h2';
static const h3 = 'h3';
static const orderedList = 'ol';
static const unorderedList = 'ul';
static const list = 'li';
static const paragraph = 'p';
static const image = 'img';
static const anchor = 'a';
static const italic = 'i';
static const em = 'em';
static const bold = 'b';
static const underline = 'u';
static const del = 'del';
static const strong = 'strong';
static const checkbox = 'input';
static const span = 'span';
static const code = 'code';
static const blockQuote = 'blockquote';
static const div = 'div';
static const divider = 'hr';
static List<String> formattingElements = [
HTMLTags.anchor,
HTMLTags.italic,
HTMLTags.em,
HTMLTags.bold,
HTMLTags.underline,
HTMLTags.del,
HTMLTags.strong,
HTMLTags.span,
HTMLTags.code,
];
static List<String> specialElements = [
HTMLTags.h1,
HTMLTags.h2,
HTMLTags.h3,
HTMLTags.unorderedList,
HTMLTags.orderedList,
HTMLTags.div,
HTMLTags.list,
HTMLTags.paragraph,
HTMLTags.blockQuote,
HTMLTags.checkbox,
HTMLTags.image
];
static bool isTopLevel(String tag) {
return tag == h1 ||
tag == h2 ||
tag == h3 ||
tag == checkbox ||
tag == paragraph ||
tag == div ||
tag == blockQuote;
}
}

View file

@ -0,0 +1,12 @@
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
class DocumentHTMLEncoder extends Converter<Document, String> {
DocumentHTMLEncoder();
@override
String convert(Document input) {
List<Node> documentNodes = input.root.children.toList();
return nodesToHTML(documentNodes);
}
}

View file

@ -0,0 +1,268 @@
import 'dart:convert';
import 'dart:ui';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:html/dom.dart' as dom;
class NodeHTMLEncoder extends Converter<List<Node>, String> {
NodeHTMLEncoder();
dom.Element? _stashListContainer;
final List<dom.Node> _result = [];
final List<Node> nodes = [];
@override
String convert(List<Node> input) {
nodes.addAll(input);
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,
children: documentNode.children,
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,
required Iterable<Node> children,
}) {
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 (children.isNotEmpty) {
for (var node in children) {
if (node.type != ImageBlockKeys.type) {
childNodes.add(
_deltaToHtml(
node.attributes[ParagraphBlockKeys.delta],
type: node.type,
attributes: node.attributes,
children: node.children,
),
);
} else {
final anchor = dom.Element.tag(HTMLTags.image);
anchor.attributes["src"] = node.attributes[ImageBlockKeys.url];
childNodes.add(_insertText(HTMLTags.span, childNodes: [anchor]));
}
}
}
if (tagName == HTMLTags.blockQuote) {
return _insertText(HTMLTags.blockQuote, childNodes: childNodes);
} else if (tagName == HTMLTags.checkbox) {
return _insertText(HTMLTags.div, childNodes: childNodes);
} else if (!HTMLTags.isTopLevel(tagName)) {
return _insertText(HTMLTags.list, childNodes: childNodes);
} 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;
}
String stringify(dom.Node node) {
if (node is dom.Element) {
return node.outerHtml;
}
if (node is dom.Text) {
return node.text;
}
return '';
}
}

View file

@ -1,10 +1,8 @@
library delta_markdown;
library html_to_document;
import 'dart:convert';
import 'package:appflowy_editor/src/core/document/document.dart';
import 'package:appflowy_editor/src/plugins/html/html_document_decoder.dart';
import 'package:appflowy_editor/src/plugins/html/html_document_encoder.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
/// Converts a html to [Document].
Document htmlToDocument(String html) {

View file

@ -0,0 +1,24 @@
library html_to_nodes;
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
List<Node> htmlToNodes(String html) {
return const AppFlowyEditorHTMLNodeCodec().decode(html);
}
/// Converts a [Document] to html.
String nodesToHTML(List<Node> nodes) {
return const AppFlowyEditorHTMLNodeCodec().encode(nodes);
}
class AppFlowyEditorHTMLNodeCodec extends Codec<List<Node>, String> {
const AppFlowyEditorHTMLNodeCodec();
@override
Converter<String, List<Node>> get decoder => NodeHTMLDecoder();
@override
Converter<List<Node>, String> get encoder => NodeHTMLEncoder();
}

View file

@ -26,31 +26,38 @@ Selection _computeSelectionAfterPasteMultipleNodes(
}
void _handleCopy(EditorState editorState) async {
final selection = editorState.cursorSelection?.normalized;
final selection = editorState.selection?.normalized;
if (selection == null || selection.isCollapsed) {
return;
}
if (selection.start.path.equals(selection.end.path)) {
final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!;
if (nodeAtPath.type == "text") {
final textNode = nodeAtPath as TextNode;
final htmlString = NodesToHTMLConverter(
nodes: [textNode],
startOffset: selection.start.offset,
endOffset: selection.end.offset,
).toHTMLString();
final textString = textNode.toPlainText().substring(
selection.startIndex,
selection.endIndex,
);
final textNode = nodeAtPath;
final htmlString = nodesToHTML([textNode]);
String textString = "";
final Delta? delta = textNode.delta;
final children = textNode.children;
final plainText = delta != null ? delta.toPlainText() : "";
if (children == null) {
textString = plainText;
} else {
final String chilrenString = children.fold('', (previousValue, node) {
final delta = node.delta;
if (delta != null) {
return previousValue + '\n' + delta.toPlainText();
}
return previousValue;
});
textString = "$plainText $chilrenString";
}
Log.keyboard.debug('copy html: $htmlString');
AppFlowyClipboard.setData(
text: textString,
html: htmlString,
);
} else {
Log.keyboard.debug('unimplemented: copy non-text');
}
return;
}
@ -63,24 +70,30 @@ void _handleCopy(EditorState editorState) async {
endNode: endNode,
).toList();
final html = NodesToHTMLConverter(
nodes: nodes,
startOffset: selection.start.offset,
endOffset: selection.end.offset,
).toHTMLString();
final html = nodesToHTML(nodes);
var text = '';
for (final node in nodes) {
if (node is TextNode) {
if (node.path == selection.start.path) {
text += node.toPlainText().substring(selection.start.offset);
} else if (node.path == selection.end.path) {
text += node.toPlainText().substring(0, selection.end.offset);
String textString = "";
final Delta? delta = node.delta;
final children = node.children;
final plainText = delta != null ? delta.toPlainText() : "";
if (children == null) {
textString = plainText;
} else {
text += node.toPlainText();
final String childrenString =
children.fold('', (previousValue, stringnode) {
final delta = node.delta;
if (delta != null) {
return previousValue + '\n' + delta.toPlainText();
}
return previousValue;
});
textString = "$plainText $childrenString";
}
text += '\n';
text = text + textString + '\n';
}
Log.keyboard.debug('copy html: $html');
AppFlowyClipboard.setData(
text: text,
html: html,
@ -88,7 +101,7 @@ void _handleCopy(EditorState editorState) async {
}
void _pasteHTML(EditorState editorState, String html) {
final selection = editorState.cursorSelection?.normalized;
final selection = editorState.selection?.normalized;
if (selection == null) {
return;
}
@ -101,35 +114,14 @@ void _pasteHTML(EditorState editorState, String html) {
}
Log.keyboard.debug('paste html: $html');
final nodes = HTMLToNodesConverter(html).toNodes();
final htmltoNodes = htmlToNodes(html);
if (nodes.isEmpty) {
return;
} else if (nodes.length == 1) {
final firstNode = nodes[0];
final nodeAtPath = editorState.document.nodeAtPath(path)!;
final tb = editorState.transaction;
final startOffset = selection.start.offset;
if (nodeAtPath.type == "text" && firstNode.type == "text") {
final textNodeAtPath = nodeAtPath as TextNode;
final firstTextNode = firstNode as TextNode;
tb.updateText(
textNodeAtPath,
(Delta()..retain(startOffset)) + firstTextNode.delta,
);
tb.updateNode(textNodeAtPath, firstTextNode.attributes);
tb.afterSelection = (Selection.collapsed(
Position(
path: path,
offset: startOffset + firstTextNode.delta.length,
),
));
editorState.apply(tb);
if (htmltoNodes.isEmpty) {
return;
}
}
_pasteMultipleLinesInText(editorState, path, selection.start.offset, nodes);
_pasteMultipleLinesInText(
editorState, path, selection.start.offset, htmltoNodes);
}
void _pasteMultipleLinesInText(

View file

@ -1,87 +0,0 @@
import 'package:appflowy_editor/src/infra/html_converter.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('HTMLTag tests', () {
test('isTopLevel', () {
const topLevelTag = HTMLTag.h1;
const nTopLevelTag = HTMLTag.strong;
expect(HTMLTag.isTopLevel(topLevelTag), true);
expect(HTMLTag.isTopLevel(nTopLevelTag), false);
});
});
group('HTMLConverter tests', () {
test('HTMLToNodesConverter and NodesToHTMLConverter', () {
final fromHTMLConverter = HTMLToNodesConverter(rawHTML);
final nodes = fromHTMLConverter.toNodes();
expect(nodes.isNotEmpty, true);
// final toHTMLConverter = NodesToHTMLConverter(nodes: nodes);
// final html = toHTMLConverter.toHTMLString();
// expect(html.isNotEmpty, true);
// expect(html, rawHTML);
});
});
group('HTMLConverterDocument tests', () {
test('HTMLToNodesConverter', () {
final converter = HTMLToNodesConverter(rawHTML);
final document = converter.toDocument();
expect(document.root.children.isNotEmpty, true);
});
});
}
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>""";

View file

@ -97,6 +97,12 @@ class TestableEditor {
);
}
void initializeWithDocment(Document document) {
_editorState = EditorState(
document: document,
);
}
Future<void> dispose() async {
_ime = null;
// Workaround: to wait all the debounce calls expire.

View file

@ -0,0 +1,545 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
void main() async {
group('html_document_decoder_test.dart', () {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
test('parser document', () async {
final result = DocumentHTMLDecoder().convert(rawHTML);
expect(result.toJson(), example);
});
test('document with quote', () async {
final result = DocumentHTMLDecoder().convert(rawHtmlWithQuote);
expect(result.toJson(), quoteExample);
});
test('nested parser document', () async {
final result = DocumentHTMLDecoder().convert(nestedhtml);
expect(result.toJson(), nestedDelta);
});
});
}
const rawHtmlWithQuote =
'''<h1>Welcome to the playground</h1><blockquote>In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.</blockquote><p>The playground is a demo environment built with <code>@lexical/react</code>. Try typing in <strong>some text</strong> with <em>different</em> formats.</p>''';
const quoteExample = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'Welcome to the playground'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert':
'In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.'
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'The playground is a demo environment built with '},
{
'insert': '@lexical/react',
'attributes': {'code': true}
},
{'insert': '. Try typing in '},
{
'insert': 'some text',
'attributes': {'bold': true}
},
{'insert': ' with '},
{
'insert': 'different',
'attributes': {'italic': true}
},
{'insert': ' formats.'}
]
}
}
]
}
};
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>[x] Customizable</li><li>[x] Test-covered</li><li>[ ] more to come!</li><li>First item</li><li>Second item</li><li>List element</li></ul><blockquote>This is a quote!</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': []}
}
]
}
};
const nestedhtml =
'''<h1>Welcome to the playground</h1><blockquote>In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting. The playground is a demo environment built with <code>@lexical/react</code>. Try typing in <strong>some text</strong> with <i>different</i> formats.</blockquote><p>\t</p><img src="https://richtexteditor.com/images/editor-image.png"><p>Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!</p><p></p><p>If you\'d like to find out more about Lexical, you can:</p><ul><li>Visit the <a href="https://lexical.dev/">Lexical website</a> for documentation and more information.</li><li>\t<span><img src="https://richtexteditor.com/images/editor-image.png"></span></li><li>Check out the code on our <a href="https://github.com/facebook/lexical">GitHub repository</a>.</li><li>Playground code can be found <a href="https://github.com/facebook/lexical/tree/main/packages/lexical-playground">here</a>.</li><li>Join our <a href="https://discord.com/invite/KmG4wQnnD9">Discord Server</a> and chat with the team.</li><li>Playground code can be found <a href="https://github.com/facebook/lexical/tree/main/packages/lexical-playground">here</a>.</li></ul><p>Lastly, we\'re constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.</p><p></p>''';
const nestedDelta = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'Welcome to the playground'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert':
'In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting. The playground is a demo environment built with '
},
{
'insert': '@lexical/react',
'attributes': {'code': true}
},
{'insert': '. Try typing in '},
{
'insert': 'some text',
'attributes': {'bold': true}
},
{'insert': ' with '},
{
'insert': 'different',
'attributes': {'italic': true}
},
{'insert': ' formats.'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': '\t'}
]
}
},
{
'type': 'image',
'data': {
'url': 'https://richtexteditor.com/images/editor-image.png',
'align': 'center',
'height': null,
'width': null
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'If you\'d like to find out more about Lexical, you can:'
}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Visit the '},
{
'insert': 'Lexical website',
'attributes': {'href': 'https://lexical.dev/'}
},
{'insert': ' for documentation and more information.'}
]
}
},
{
'type': 'bulleted_list',
'children': [
{
'type': 'image',
'data': {
'url': 'https://richtexteditor.com/images/editor-image.png',
'align': 'center',
'height': null,
'width': null
}
}
],
'data': {
'delta': [
{'insert': '\t'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Check out the code on our '},
{
'insert': 'GitHub repository',
'attributes': {'href': 'https://github.com/facebook/lexical'}
},
{'insert': '.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Playground code can be found '},
{
'insert': 'here',
'attributes': {
'href':
'https://github.com/facebook/lexical/tree/main/packages/lexical-playground'
}
},
{'insert': '.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Join our '},
{
'insert': 'Discord Server',
'attributes': {'href': 'https://discord.com/invite/KmG4wQnnD9'}
},
{'insert': ' and chat with the team.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Playground code can be found '},
{
'insert': 'here',
'attributes': {
'href':
'https://github.com/facebook/lexical/tree/main/packages/lexical-playground'
}
},
{'insert': '.'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'Lastly, we\'re constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
}
]
}
};

View file

@ -0,0 +1,547 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
void main() async {
group('html_nodes_decoder_test.dart', () {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
test('parser document', () async {
final result = NodeHTMLDecoder().convert(rawHTML);
expect(Document(root: Node(type: "page", children: result)).toJson(),
example);
});
test('quote document', () async {
final result = NodeHTMLDecoder().convert(rawHtmlWithQuote);
expect(result.map((e) => e.toJson()).toList(), quoteExample);
});
test('nested parser document', () async {
final result = NodeHTMLDecoder().convert(nestedhtml);
expect(Document(root: Node(type: "page", children: result)).toJson(),
nestedDelta);
});
test('empty document', () async {
final result = NodeHTMLDecoder().convert("");
expect(result, []);
});
});
}
const rawHtmlWithQuote =
'''<h1>Welcome to the playground</h1><blockquote>In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.</blockquote><p>The playground is a demo environment built with <code>@lexical/react</code>. Try typing in <strong>some text</strong> with <em>different</em> formats.</p>''';
const quoteExample = [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'Welcome to the playground'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert':
'In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.'
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'The playground is a demo environment built with '},
{
'insert': '@lexical/react',
'attributes': {'code': true}
},
{'insert': '. Try typing in '},
{
'insert': 'some text',
'attributes': {'bold': true}
},
{'insert': ' with '},
{
'insert': 'different',
'attributes': {'italic': true}
},
{'insert': ' formats.'}
]
}
}
];
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>[x] Customizable</li><li>[x] Test-covered</li><li>[ ] more to come!</li><li>First item</li><li>Second item</li><li>List element</li></ul><blockquote>This is a quote!</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': []}
}
]
}
};
const nestedhtml =
'''<h1>Welcome to the playground</h1><blockquote>In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting. The playground is a demo environment built with <code>@lexical/react</code>. Try typing in <strong>some text</strong> with <i>different</i> formats.</blockquote><p>\t</p><img src="https://richtexteditor.com/images/editor-image.png"><p>Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!</p><p></p><p>If you\'d like to find out more about Lexical, you can:</p><ul><li>Visit the <a href="https://lexical.dev/">Lexical website</a> for documentation and more information.</li><li>\t<span><img src="https://richtexteditor.com/images/editor-image.png"></span></li><li>Check out the code on our <a href="https://github.com/facebook/lexical">GitHub repository</a>.</li><li>Playground code can be found <a href="https://github.com/facebook/lexical/tree/main/packages/lexical-playground">here</a>.</li><li>Join our <a href="https://discord.com/invite/KmG4wQnnD9">Discord Server</a> and chat with the team.</li><li>Playground code can be found <a href="https://github.com/facebook/lexical/tree/main/packages/lexical-playground">here</a>.</li></ul><p>Lastly, we\'re constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.</p><p></p>''';
const nestedDelta = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'Welcome to the playground'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert':
'In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting. The playground is a demo environment built with '
},
{
'insert': '@lexical/react',
'attributes': {'code': true}
},
{'insert': '. Try typing in '},
{
'insert': 'some text',
'attributes': {'bold': true}
},
{'insert': ' with '},
{
'insert': 'different',
'attributes': {'italic': true}
},
{'insert': ' formats.'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': '\t'}
]
}
},
{
'type': 'image',
'data': {
'url': 'https://richtexteditor.com/images/editor-image.png',
'align': 'center',
'height': null,
'width': null
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'If you\'d like to find out more about Lexical, you can:'
}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Visit the '},
{
'insert': 'Lexical website',
'attributes': {'href': 'https://lexical.dev/'}
},
{'insert': ' for documentation and more information.'}
]
}
},
{
'type': 'bulleted_list',
'children': [
{
'type': 'image',
'data': {
'url': 'https://richtexteditor.com/images/editor-image.png',
'align': 'center',
'height': null,
'width': null
}
}
],
'data': {
'delta': [
{'insert': '\t'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Check out the code on our '},
{
'insert': 'GitHub repository',
'attributes': {'href': 'https://github.com/facebook/lexical'}
},
{'insert': '.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Playground code can be found '},
{
'insert': 'here',
'attributes': {
'href':
'https://github.com/facebook/lexical/tree/main/packages/lexical-playground'
}
},
{'insert': '.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Join our '},
{
'insert': 'Discord Server',
'attributes': {'href': 'https://discord.com/invite/KmG4wQnnD9'}
},
{'insert': ' and chat with the team.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Playground code can be found '},
{
'insert': 'here',
'attributes': {
'href':
'https://github.com/facebook/lexical/tree/main/packages/lexical-playground'
}
},
{'insert': '.'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'Lastly, we\'re constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
}
]
}
};

View file

@ -0,0 +1,698 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
void main() async {
group('html_document_encoder_test.dart', () {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
test('parser document', () async {
final result = DocumentHTMLEncoder().convert(Document.fromJson(delta));
expect(result, example);
});
test('nested parser document', () async {
final result =
DocumentHTMLEncoder().convert(Document.fromJson(nestedDelta));
expect(result, nestedhtml);
});
test('checkBox parser document', () async {
final result =
DocumentHTMLEncoder().convert(Document.fromJson(checkBoxExample));
expect(result, checkBoxHtml);
});
});
}
const checkBoxExample = {
"document": {
"type": "page",
"children": [
{
"type": "heading",
"data": {
"level": 2,
"delta": [
{"insert": "👋 "},
{
"insert": "Welcome to",
"data": {"bold": true}
},
{"insert": " "},
{
"insert": "AppFlowy Editor",
"data": {"href": "appflowy.io", "italic": true, "bold": true}
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "AppFlowy Editor is a"},
{"insert": " "},
{
"insert": "highly customizable",
"data": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"data": {"italic": true}
},
{"insert": " for "},
{
"insert": "Flutter",
"data": {"underline": true}
}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Customizable"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Test-covered"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "more to come!"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "quote",
"data": {
"delta": [
{"insert": "Here is an example you can give a try"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "You can also use "},
{
"insert": "AppFlowy Editor",
"data": {
"italic": true,
"bold": true,
"textColor": "0xffD70040",
"highlightColor": "0x6000BCF0"
}
},
{"insert": " as a component to build your own app."}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{"insert": "Use / to insert blocks"}
]
}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{
"insert":
"Select text to trigger to the toolbar to format your notes."
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"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!"
}
]
}
}
]
}
};
const checkBoxHtml =
'''<h2>👋 Welcome to AppFlowy Editor</h2><p></p><p>AppFlowy Editor is a highly customizable rich-text editor for Flutter</p><div><input type="checkbox" checked="true">Customizable</div><div><input type="checkbox" checked="true">Test-covered</div><div><input type="checkbox" checked="true">more to come!</div><p></p><blockquote>Here is an example you can give a try</blockquote><p></p><p>You can also use AppFlowy Editor as a component to build your own app.</p><p></p><ul><li>Use / to insert blocks</li><li>Select text to trigger to the toolbar to format your notes.</li></ul><p></p><p>If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!</p>''';
const rawHtmlWithQuote =
'''<h1>Welcome to the playground</h1><blockquote>In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.</blockquote><p>The playground is a demo environment built with <code>@lexical/react</code>. Try typing in <strong>some text</strong> with <i>different</i> formats.</p>''';
const quoteExample = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'Welcome to the playground'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert':
'In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.'
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'The playground is a demo environment built with '},
{
'insert': '@lexical/react',
'attributes': {'code': true}
},
{'insert': '. Try typing in '},
{
'insert': 'some text',
'attributes': {'bold': true}
},
{'insert': ' with '},
{
'insert': 'different',
'attributes': {'italic': true}
},
{'insert': ' formats.'}
]
}
}
]
}
};
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>[x] Customizable</li><li>[x] Test-covered</li><li>[ ] more to come!</li><li>First item</li><li>Second item</li><li>List element</li></ul><blockquote>This is a quote!</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': []}
}
]
}
};
const nestedhtml =
'''<h1>Welcome to the playground</h1><blockquote>In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting. The playground is a demo environment built with <code>@lexical/react</code>. Try typing in <strong>some text</strong> with <i>different</i> formats.</blockquote><p>\t</p><img src="https://richtexteditor.com/images/editor-image.png"><p>Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!</p><p></p><p>If you\'d like to find out more about Lexical, you can:</p><ul><li>Visit the <a href="https://lexical.dev/">Lexical website</a> for documentation and more information.</li><li>\t<span><img src="https://richtexteditor.com/images/editor-image.png"></span></li><li>Check out the code on our <a href="https://github.com/facebook/lexical">GitHub repository</a>.</li><li>Playground code can be found <a href="https://github.com/facebook/lexical/tree/main/packages/lexical-playground">here</a>.</li><li>Join our <a href="https://discord.com/invite/KmG4wQnnD9">Discord Server</a> and chat with the team.</li><li>Playground code can be found <a href="https://github.com/facebook/lexical/tree/main/packages/lexical-playground">here</a>.</li></ul><p>Lastly, we\'re constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.</p><p></p>''';
const nestedDelta = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'Welcome to the playground'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert':
'In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting. The playground is a demo environment built with '
},
{
'insert': '@lexical/react',
'attributes': {'code': true}
},
{'insert': '. Try typing in '},
{
'insert': 'some text',
'attributes': {'bold': true}
},
{'insert': ' with '},
{
'insert': 'different',
'attributes': {'italic': true}
},
{'insert': ' formats.'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': '\t'}
]
}
},
{
'type': 'image',
'data': {
'url': 'https://richtexteditor.com/images/editor-image.png',
'align': 'center',
'height': null,
'width': null
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'If you\'d like to find out more about Lexical, you can:'
}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Visit the '},
{
'insert': 'Lexical website',
'attributes': {'href': 'https://lexical.dev/'}
},
{'insert': ' for documentation and more information.'}
]
}
},
{
'type': 'bulleted_list',
'children': [
{
'type': 'image',
'data': {
'url': 'https://richtexteditor.com/images/editor-image.png',
'align': 'center',
'height': null,
'width': null
}
}
],
'data': {
'delta': [
{'insert': '\t'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Check out the code on our '},
{
'insert': 'GitHub repository',
'attributes': {'href': 'https://github.com/facebook/lexical'}
},
{'insert': '.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Playground code can be found '},
{
'insert': 'here',
'attributes': {
'href':
'https://github.com/facebook/lexical/tree/main/packages/lexical-playground'
}
},
{'insert': '.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Join our '},
{
'insert': 'Discord Server',
'attributes': {'href': 'https://discord.com/invite/KmG4wQnnD9'}
},
{'insert': ' and chat with the team.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Playground code can be found '},
{
'insert': 'here',
'attributes': {
'href':
'https://github.com/facebook/lexical/tree/main/packages/lexical-playground'
}
},
{'insert': '.'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'Lastly, we\'re constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
}
]
}
};

View file

@ -0,0 +1,697 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
void main() async {
group('html_nodes_encoder_test.dart', () {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
test('parser document', () async {
final result = NodeHTMLEncoder()
.convert(Document.fromJson(delta).root.children.toList());
expect(result, example);
});
test('nested parser document', () async {
final result = NodeHTMLEncoder()
.convert(Document.fromJson(nestedDelta).root.children.toList());
expect(result, nestedhtml);
});
test('quote parser document', () async {
final result = NodeHTMLEncoder()
.convert(quoteExample.map((e) => Node.fromJson(e)).toList());
expect(result, rawHtmlWithQuote);
});
test('checkBox parser document', () async {
final result = NodeHTMLEncoder()
.convert(checkBoxExample.map((e) => Node.fromJson(e)).toList());
expect(result, checkBoxHtml);
});
});
}
const checkBoxExample = [
{
"type": "heading",
"data": {
"level": 2,
"delta": [
{"insert": "👋 "},
{
"insert": "Welcome to",
"data": {"bold": true}
},
{"insert": " "},
{
"insert": "AppFlowy Editor",
"data": {"href": "appflowy.io", "italic": true, "bold": true}
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "AppFlowy Editor is a"},
{"insert": " "},
{
"insert": "highly customizable",
"data": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"data": {"italic": true}
},
{"insert": " for "},
{
"insert": "Flutter",
"data": {"underline": true}
}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Customizable"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Test-covered"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "more to come!"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "quote",
"data": {
"delta": [
{"insert": "Here is an example you can give a try"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "You can also use "},
{
"insert": "AppFlowy Editor",
"data": {
"italic": true,
"bold": true,
"textColor": "0xffD70040",
"highlightColor": "0x6000BCF0"
}
},
{"insert": " as a component to build your own app."}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{"insert": "Use / to insert blocks"}
]
}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{
"insert":
"Select text to trigger to the toolbar to format your notes."
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"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!"
}
]
}
}
];
const checkBoxHtml =
'''<h2>👋 Welcome to AppFlowy Editor</h2><p></p><p>AppFlowy Editor is a highly customizable rich-text editor for Flutter</p><div><input type="checkbox" checked="true">Customizable</div><div><input type="checkbox" checked="true">Test-covered</div><div><input type="checkbox" checked="true">more to come!</div><p></p><blockquote>Here is an example you can give a try</blockquote><p></p><p>You can also use AppFlowy Editor as a component to build your own app.</p><p></p><ul><li>Use / to insert blocks</li><li>Select text to trigger to the toolbar to format your notes.</li></ul><p></p><p>If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!</p>''';
const rawHtmlWithQuote =
'''<h1>Welcome to the playground</h1><blockquote>In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.</blockquote><p>The playground is a demo environment built with <code>@lexical/react</code>. Try typing in <strong>some text</strong> with <i>different</i> formats.</p>''';
const quoteExample = [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'Welcome to the playground'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert':
'In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.'
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'The playground is a demo environment built with '},
{
'insert': '@lexical/react',
'attributes': {'code': true}
},
{'insert': '. Try typing in '},
{
'insert': 'some text',
'attributes': {'bold': true}
},
{'insert': ' with '},
{
'insert': 'different',
'attributes': {'italic': true}
},
{'insert': ' formats.'}
]
}
}
];
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>[x] Customizable</li><li>[x] Test-covered</li><li>[ ] more to come!</li><li>First item</li><li>Second item</li><li>List element</li></ul><blockquote>This is a quote!</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': []}
}
]
}
};
const nestedhtml =
'''<h1>Welcome to the playground</h1><blockquote>In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting. The playground is a demo environment built with <code>@lexical/react</code>. Try typing in <strong>some text</strong> with <i>different</i> formats.</blockquote><p>\t</p><img src="https://richtexteditor.com/images/editor-image.png"><p>Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!</p><p></p><p>If you\'d like to find out more about Lexical, you can:</p><ul><li>Visit the <a href="https://lexical.dev/">Lexical website</a> for documentation and more information.</li><li>\t<span><img src="https://richtexteditor.com/images/editor-image.png"></span></li><li>Check out the code on our <a href="https://github.com/facebook/lexical">GitHub repository</a>.</li><li>Playground code can be found <a href="https://github.com/facebook/lexical/tree/main/packages/lexical-playground">here</a>.</li><li>Join our <a href="https://discord.com/invite/KmG4wQnnD9">Discord Server</a> and chat with the team.</li><li>Playground code can be found <a href="https://github.com/facebook/lexical/tree/main/packages/lexical-playground">here</a>.</li></ul><p>Lastly, we\'re constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.</p><p></p>''';
const nestedDelta = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'Welcome to the playground'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert':
'In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting. The playground is a demo environment built with '
},
{
'insert': '@lexical/react',
'attributes': {'code': true}
},
{'insert': '. Try typing in '},
{
'insert': 'some text',
'attributes': {'bold': true}
},
{'insert': ' with '},
{
'insert': 'different',
'attributes': {'italic': true}
},
{'insert': ' formats.'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': '\t'}
]
}
},
{
'type': 'image',
'data': {
'url': 'https://richtexteditor.com/images/editor-image.png',
'align': 'center',
'height': null,
'width': null
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'If you\'d like to find out more about Lexical, you can:'
}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Visit the '},
{
'insert': 'Lexical website',
'attributes': {'href': 'https://lexical.dev/'}
},
{'insert': ' for documentation and more information.'}
]
}
},
{
'type': 'bulleted_list',
'children': [
{
'type': 'image',
'data': {
'url': 'https://richtexteditor.com/images/editor-image.png',
'align': 'center',
'height': null,
'width': null
}
}
],
'data': {
'delta': [
{'insert': '\t'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Check out the code on our '},
{
'insert': 'GitHub repository',
'attributes': {'href': 'https://github.com/facebook/lexical'}
},
{'insert': '.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Playground code can be found '},
{
'insert': 'here',
'attributes': {
'href':
'https://github.com/facebook/lexical/tree/main/packages/lexical-playground'
}
},
{'insert': '.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Join our '},
{
'insert': 'Discord Server',
'attributes': {'href': 'https://discord.com/invite/KmG4wQnnD9'}
},
{'insert': ' and chat with the team.'}
]
}
},
{
'type': 'bulleted_list',
'data': {
'delta': [
{'insert': 'Playground code can be found '},
{
'insert': 'here',
'attributes': {
'href':
'https://github.com/facebook/lexical/tree/main/packages/lexical-playground'
}
},
{'insert': '.'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'Lastly, we\'re constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.'
}
]
}
},
{
'type': 'paragraph',
'data': {'delta': []}
}
]
}
};

View file

@ -1,6 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'dart:developer';
void main() {
group('html_document_test.dart tests', () {
@ -24,9 +23,225 @@ void main() {
expect(document, nestedhtml);
});
test('quote documentToHtml()', () {
final document = documentToHTML(Document.fromJson(quoteExample));
expect(document, rawHtmlWithQuote);
});
test('checkBox parser document', () async {
final document = documentToHTML(Document.fromJson(checkBoxExample));
expect(document, checkBoxHtml);
});
});
}
const checkBoxExample = {
"document": {
"type": "page",
"children": [
{
"type": "heading",
"data": {
"level": 2,
"delta": [
{"insert": "👋 "},
{
"insert": "Welcome to",
"data": {"bold": true}
},
{"insert": " "},
{
"insert": "AppFlowy Editor",
"data": {"href": "appflowy.io", "italic": true, "bold": true}
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "AppFlowy Editor is a"},
{"insert": " "},
{
"insert": "highly customizable",
"data": {"bold": true}
},
{"insert": " "},
{
"insert": "rich-text editor",
"data": {"italic": true}
},
{"insert": " for "},
{
"insert": "Flutter",
"data": {"underline": true}
}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Customizable"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "Test-covered"}
]
}
},
{
"type": "todo_list",
"data": {
"checked": true,
"delta": [
{"insert": "more to come!"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "quote",
"data": {
"delta": [
{"insert": "Here is an example you can give a try"}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "paragraph",
"data": {
"delta": [
{"insert": "You can also use "},
{
"insert": "AppFlowy Editor",
"data": {
"italic": true,
"bold": true,
"textColor": "0xffD70040",
"highlightColor": "0x6000BCF0"
}
},
{"insert": " as a component to build your own app."}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{"insert": "Use / to insert blocks"}
]
}
},
{
"type": "bulleted_list",
"data": {
"delta": [
{
"insert":
"Select text to trigger to the toolbar to format your notes."
}
]
}
},
{
"type": "paragraph",
"data": {"delta": []}
},
{
"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!"
}
]
}
}
]
}
};
const checkBoxHtml =
'''<h2>👋 Welcome to AppFlowy Editor</h2><p></p><p>AppFlowy Editor is a highly customizable rich-text editor for Flutter</p><div><input type="checkbox" checked="true">Customizable</div><div><input type="checkbox" checked="true">Test-covered</div><div><input type="checkbox" checked="true">more to come!</div><p></p><blockquote>Here is an example you can give a try</blockquote><p></p><p>You can also use AppFlowy Editor as a component to build your own app.</p><p></p><ul><li>Use / to insert blocks</li><li>Select text to trigger to the toolbar to format your notes.</li></ul><p></p><p>If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!</p>''';
const rawHtmlWithQuote =
'''<h1>Welcome to the playground</h1><blockquote>In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.</blockquote><p>The playground is a demo environment built with <code>@lexical/react</code>. Try typing in <strong>some text</strong> with <i>different</i> formats.</p>''';
const quoteExample = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'Welcome to the playground'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert':
'In case you were wondering what the black box at the bottom is – it\'s the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.'
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'The playground is a demo environment built with '},
{
'insert': '@lexical/react',
'attributes': {'code': true}
},
{'insert': '. Try typing in '},
{
'insert': 'some text',
'attributes': {'bold': true}
},
{'insert': ' with '},
{
'insert': 'different',
'attributes': {'italic': true}
},
{'insert': ' formats.'}
]
}
}
]
}
};
const data = {
'document': {
'type': 'page',

Some files were not shown because too many files have changed in this diff Show more