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:
parent
2d3ab80c8f
commit
ad838cfa86
22 changed files with 2829 additions and 879 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
}
|
||||
16
lib/src/plugins/html/decoder/html_document_decoder.dart
Normal file
16
lib/src/plugins/html/decoder/html_document_decoder.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
351
lib/src/plugins/html/decoder/html_nodes_decoder.dart
Normal file
351
lib/src/plugins/html/decoder/html_nodes_decoder.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
12
lib/src/plugins/html/encoder/html_document_encoder.dart
Normal file
12
lib/src/plugins/html/encoder/html_document_encoder.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
268
lib/src/plugins/html/encoder/html_nodes_encoder.dart
Normal file
268
lib/src/plugins/html/encoder/html_nodes_encoder.dart
Normal 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 '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
24
lib/src/plugins/html/html_nodes.dart
Normal file
24
lib/src/plugins/html/html_nodes.dart
Normal 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();
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>""";
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
545
test/plugins/html/decoder/html_document_decoder_test.dart
Normal file
545
test/plugins/html/decoder/html_document_decoder_test.dart
Normal 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': []}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
547
test/plugins/html/decoder/html_nodes_decoder_test.dart
Normal file
547
test/plugins/html/decoder/html_nodes_decoder_test.dart
Normal 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': []}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
698
test/plugins/html/encoder/html_document_encoder_test.dart
Normal file
698
test/plugins/html/encoder/html_document_encoder_test.dart
Normal 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': []}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
697
test/plugins/html/encoder/html_nodes_encoder_test.dart
Normal file
697
test/plugins/html/encoder/html_nodes_encoder_test.dart
Normal 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': []}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
|
@ -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
Loading…
Reference in a new issue