diff --git a/lib/src/infra/html_converter.dart b/lib/src/infra/html_converter.dart index e82a0b105..1d80da4c1 100644 --- a/lib/src/infra/html_converter.dart +++ b/lib/src/infra/html_converter.dart @@ -177,7 +177,7 @@ class HTMLToNodesConverter { return result; } - Attributes? _getDeltaAttributesFromHtmlAttributes( + Attributes? _getDeltaAttributesFromHTMLAttributes( LinkedHashMap htmlAttributes, ) { final attrs = {}; @@ -231,7 +231,7 @@ class HTMLToNodesConverter { if (element.localName == HTMLTag.span) { delta.insert( element.text, - attributes: _getDeltaAttributesFromHtmlAttributes(element.attributes), + attributes: _getDeltaAttributesFromHTMLAttributes(element.attributes), ); } else if (element.localName == HTMLTag.anchor) { final hyperLink = element.attributes['href']; @@ -542,7 +542,7 @@ class NodesToHTMLConverter { /// ```html /// Text /// ``` - // html.Element _deltaToHtml( + // html.Element _deltaToHTML( // Delta delta, { // String? subType, // String? heading, diff --git a/lib/src/plugins/html/encoder/delta_html_encoder.dart b/lib/src/plugins/html/encoder/delta_html_encoder.dart new file mode 100644 index 000000000..ea6eb376c --- /dev/null +++ b/lib/src/plugins/html/encoder/delta_html_encoder.dart @@ -0,0 +1,152 @@ +import 'dart:convert'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:html/dom.dart' as dom; + +final deltaHTMLEncoder = DeltaHTMLEncoder(); + +/// A [Delta] encoder that encodes a [Delta] to html. +/// +/// supported nested styles. +class DeltaHTMLEncoder extends Converter> { + @override + List convert(Delta input) { + return input + .whereType() + .map(convertTextInsertToDomNode) + .toList(); + } + + dom.Node convertTextInsertToDomNode(TextInsert textInsert) { + final text = textInsert.text; + final attributes = textInsert.attributes; + + if (attributes == null) { + return dom.Text(text); + } + + // if there is only one attribute, we can use the tag directly + if (attributes.length == 1) { + return convertSingleAttributeTextInsertToDomNode(text, attributes); + } + + return convertMultipleAttributeTextInsertToDomNode(text, attributes); + } + + dom.Element convertSingleAttributeTextInsertToDomNode( + String text, + Attributes attributes, + ) { + assert(attributes.length == 1); + + final domText = dom.Text(text); + + // href is a special case, it should be an anchor tag + final href = attributes.href; + if (href != null) { + return dom.Element.tag(HTMLTags.anchor) + ..attributes['href'] = href + ..append(domText); + } + + final keyToTag = { + AppFlowyRichTextKeys.bold: HTMLTags.strong, + AppFlowyRichTextKeys.italic: HTMLTags.italic, + AppFlowyRichTextKeys.underline: HTMLTags.underline, + AppFlowyRichTextKeys.strikethrough: HTMLTags.del, + AppFlowyRichTextKeys.code: HTMLTags.code, + null: HTMLTags.paragraph, + }; + + final tag = keyToTag[attributes.keys.first]; + return dom.Element.tag(tag)..append(domText); + } + + dom.Element convertMultipleAttributeTextInsertToDomNode( + String text, + Attributes attributes, + ) { + //rich editor for webs do this so handling that case for href demo + final element = hrefEdgeCaseAttributes(text, attributes); + if (element != null) { + return element; + } + final span = dom.Element.tag(HTMLTags.span); + final cssString = convertAttributesToCssStyle(attributes); + if (cssString.isNotEmpty) { + span.attributes['style'] = cssString; + } + span.append(dom.Text(text)); + return span; + } + + dom.Element? hrefEdgeCaseAttributes( + String text, + Attributes attributes, + ) { + final href = attributes[AppFlowyRichTextKeys.href]; + if (href == null) { + return null; + } + final element = dom.Element.tag(HTMLTags.anchor)..attributes['href'] = href; + dom.Element? newElement; + dom.Element? nestedElement; + + for (final entry in attributes.entries) { + final key = entry.key; + final value = entry.value; + + if (key == AppFlowyRichTextKeys.href) { + continue; + } + + final appendElement = convertSingleAttributeTextInsertToDomNode( + newElement == null ? text : '', + {key: value}, + ); + + if (newElement == null) { + newElement = appendElement; + } else { + nestedElement = appendElement..append(newElement); + newElement = nestedElement; + } + } + + if (newElement != null) { + element.append(newElement); + } + + return element; + } + + String convertAttributesToCssStyle(Map attributes) { + final cssMap = {}; + + if (attributes.bold) { + cssMap['font-weight'] = 'bold'; + } + + if (attributes.underline) { + cssMap['text-decoration'] = 'underline'; + } else if (attributes.strikethrough) { + cssMap['text-decoration'] = 'line-through'; + } + + if (attributes.italic) { + cssMap['font-style'] = 'italic'; + } + + final backgroundColor = attributes.backgroundColor; + if (backgroundColor != null) { + cssMap['background-color'] = backgroundColor.toRgbaString(); + } + + final color = attributes.color; + if (color != null) { + cssMap['color'] = color.toRgbaString(); + } + + return cssMap.entries.map((e) => '${e.key}: ${e.value}').join('; '); + } +} diff --git a/lib/src/plugins/html/encoder/parser/bulleted_list_node_parser.dart b/lib/src/plugins/html/encoder/parser/bulleted_list_node_parser.dart new file mode 100644 index 000000000..104506af4 --- /dev/null +++ b/lib/src/plugins/html/encoder/parser/bulleted_list_node_parser.dart @@ -0,0 +1,42 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:html/dom.dart' as dom; + +class HTMLBulletedListNodeParser extends HTMLNodeParser { + const HTMLBulletedListNodeParser(); + + @override + String get id => BulletedListBlockKeys.type; + + @override + String transformNodeToHTMLString( + Node node, { + required List encodeParsers, + }) { + assert(node.type == BulletedListBlockKeys.type); + + return toHTMLString( + transformNodeToDomNodes(node, encodeParsers: encodeParsers), + ); + } + + @override + List transformNodeToDomNodes( + Node node, { + required List encodeParsers, + }) { + final delta = node.delta ?? Delta(); + final domNodes = deltaHTMLEncoder.convert(delta); + domNodes.addAll( + processChildrenNodes( + node.children, + encodeParsers: encodeParsers, + ), + ); + + final element = + wrapChildrenNodesWithTagName(HTMLTags.list, childNodes: domNodes); + return [ + dom.Element.tag(HTMLTags.unorderedList)..append(element), + ]; + } +} diff --git a/lib/src/plugins/html/encoder/parser/heading_node_parser.dart b/lib/src/plugins/html/encoder/parser/heading_node_parser.dart new file mode 100644 index 000000000..875c73821 --- /dev/null +++ b/lib/src/plugins/html/encoder/parser/heading_node_parser.dart @@ -0,0 +1,35 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:html/dom.dart' as dom; + +class HTMLHeadingNodeParser extends HTMLNodeParser { + const HTMLHeadingNodeParser(); + + @override + String get id => HeadingBlockKeys.type; + + @override + String transformNodeToHTMLString( + Node node, { + required List encodeParsers, + }) { + return toHTMLString( + transformNodeToDomNodes(node, encodeParsers: encodeParsers), + ); + } + + @override + List transformNodeToDomNodes( + Node node, { + required List encodeParsers, + }) { + final delta = node.delta ?? Delta(); + final convertedNodes = deltaHTMLEncoder.convert(delta); + convertedNodes.addAll( + processChildrenNodes(node.children, encodeParsers: encodeParsers), + ); + final tagName = 'h${node.attributes[HeadingBlockKeys.level]}'; + final element = + wrapChildrenNodesWithTagName(tagName, childNodes: convertedNodes); + return [element]; + } +} diff --git a/lib/src/plugins/html/encoder/parser/html_node_parser.dart b/lib/src/plugins/html/encoder/parser/html_node_parser.dart new file mode 100644 index 000000000..8016e0972 --- /dev/null +++ b/lib/src/plugins/html/encoder/parser/html_node_parser.dart @@ -0,0 +1,57 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:collection/collection.dart'; +import 'package:html/dom.dart' as dom; + +abstract class HTMLNodeParser { + const HTMLNodeParser(); + + /// The id of the node parser. + /// + /// Basically, it's the type of the node. + String get id; + + /// Transform the [node] to html string. + String transformNodeToHTMLString( + Node node, { + required List encodeParsers, + }); + + /// Convert the [node] to html nodes. + List transformNodeToDomNodes( + Node node, { + required List encodeParsers, + }); + + dom.Element wrapChildrenNodesWithTagName( + String tagName, { + required List childNodes, + }) { + final p = dom.Element.tag(tagName); + for (final node in childNodes) { + p.append(node); + } + return p; + } + + // iterate over its children if exist + List processChildrenNodes( + Iterable nodes, { + required List encodeParsers, + }) { + final result = []; + for (final node in nodes) { + final parser = encodeParsers.firstWhereOrNull( + (element) => element.id == node.type, + ); + if (parser != null) { + result.addAll( + parser.transformNodeToDomNodes(node, encodeParsers: encodeParsers), + ); + } + } + return result; + } + + String toHTMLString(List nodes) => + nodes.map((e) => stringify(e)).join().replaceAll('\n', ''); +} diff --git a/lib/src/plugins/html/encoder/parser/html_parser.dart b/lib/src/plugins/html/encoder/parser/html_parser.dart new file mode 100644 index 000000000..e394f24dc --- /dev/null +++ b/lib/src/plugins/html/encoder/parser/html_parser.dart @@ -0,0 +1,8 @@ +export 'bulleted_list_node_parser.dart'; +export 'heading_node_parser.dart'; +export 'html_node_parser.dart'; +export 'image_node_parser.dart'; +export 'numbered_list_node_parser.dart'; +export 'quote_node_parser.dart'; +export 'text_node_parser.dart'; +export 'todo_list_node_parser.dart'; diff --git a/lib/src/plugins/html/encoder/parser/image_node_parser.dart b/lib/src/plugins/html/encoder/parser/image_node_parser.dart new file mode 100644 index 000000000..20d965a37 --- /dev/null +++ b/lib/src/plugins/html/encoder/parser/image_node_parser.dart @@ -0,0 +1,51 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:html/dom.dart' as dom; + +class HTMLImageNodeParser extends HTMLNodeParser { + const HTMLImageNodeParser(); + + @override + String get id => ImageBlockKeys.type; + + @override + String transformNodeToHTMLString( + Node node, { + required List encodeParsers, + }) { + return toHTMLString( + transformNodeToDomNodes(node, encodeParsers: encodeParsers), + ); + } + + @override + List transformNodeToDomNodes( + Node node, { + required List encodeParsers, + }) { + final anchor = dom.Element.tag(HTMLTags.image); + anchor.attributes['src'] = node.attributes[ImageBlockKeys.url]; + + final height = node.attributes[ImageBlockKeys.height]; + if (height != null) { + anchor.attributes['height'] = height; + } + + final width = node.attributes[ImageBlockKeys.width]; + if (width != null) { + anchor.attributes['width'] = width; + } + + final align = node.attributes[ImageBlockKeys.align]; + if (align != null) { + anchor.attributes['align'] = align; + } + + return [ + anchor, + ...processChildrenNodes( + node.children.toList(), + encodeParsers: encodeParsers, + ), + ]; + } +} diff --git a/lib/src/plugins/html/encoder/parser/numbered_list_node_parser.dart b/lib/src/plugins/html/encoder/parser/numbered_list_node_parser.dart new file mode 100644 index 000000000..e4d245bce --- /dev/null +++ b/lib/src/plugins/html/encoder/parser/numbered_list_node_parser.dart @@ -0,0 +1,39 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:html/dom.dart' as dom; + +class HTMLNumberedListNodeParser extends HTMLNodeParser { + const HTMLNumberedListNodeParser(); + + @override + String get id => NumberedListBlockKeys.type; + + @override + String transformNodeToHTMLString( + Node node, { + required List encodeParsers, + }) { + assert(node.type == NumberedListBlockKeys.type); + + return toHTMLString( + transformNodeToDomNodes(node, encodeParsers: encodeParsers), + ); + } + + @override + List transformNodeToDomNodes( + Node node, { + required List encodeParsers, + }) { + final delta = node.delta ?? Delta(); + final domNodes = deltaHTMLEncoder.convert(delta); + domNodes.addAll( + processChildrenNodes(node.children, encodeParsers: encodeParsers), + ); + + final element = + wrapChildrenNodesWithTagName(HTMLTags.list, childNodes: domNodes); + return [ + dom.Element.tag(HTMLTags.orderedList)..append(element), + ]; + } +} diff --git a/lib/src/plugins/html/encoder/parser/quote_node_parser.dart b/lib/src/plugins/html/encoder/parser/quote_node_parser.dart new file mode 100644 index 000000000..efff024b9 --- /dev/null +++ b/lib/src/plugins/html/encoder/parser/quote_node_parser.dart @@ -0,0 +1,37 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:html/dom.dart' as dom; + +class HTMLQuoteNodeParser extends HTMLNodeParser { + const HTMLQuoteNodeParser(); + + @override + String get id => QuoteBlockKeys.type; + + @override + String transformNodeToHTMLString( + Node node, { + required List encodeParsers, + }) { + assert(node.type == QuoteBlockKeys.type); + + return toHTMLString( + transformNodeToDomNodes(node, encodeParsers: encodeParsers), + ); + } + + @override + List transformNodeToDomNodes( + Node node, { + required List encodeParsers, + }) { + final delta = node.delta ?? Delta(); + final domNodes = deltaHTMLEncoder.convert(delta); + domNodes.addAll( + processChildrenNodes(node.children, encodeParsers: encodeParsers), + ); + + final element = + wrapChildrenNodesWithTagName(HTMLTags.blockQuote, childNodes: domNodes); + return [element]; + } +} diff --git a/lib/src/plugins/html/encoder/parser/text_node_parser.dart b/lib/src/plugins/html/encoder/parser/text_node_parser.dart new file mode 100644 index 000000000..88f2d1198 --- /dev/null +++ b/lib/src/plugins/html/encoder/parser/text_node_parser.dart @@ -0,0 +1,38 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:html/dom.dart' as dom; + +class HTMLTextNodeParser extends HTMLNodeParser { + const HTMLTextNodeParser(); + + @override + String get id => ParagraphBlockKeys.type; + + @override + String transformNodeToHTMLString( + Node node, { + required List encodeParsers, + }) { + return toHTMLString( + transformNodeToDomNodes(node, encodeParsers: encodeParsers), + ); + } + + @override + List transformNodeToDomNodes( + Node node, { + required List encodeParsers, + }) { + final delta = node.delta ?? Delta(); + final domNodes = deltaHTMLEncoder.convert(delta); + domNodes.addAll( + processChildrenNodes( + node.children.toList(), + encodeParsers: encodeParsers, + ), + ); + + final element = + wrapChildrenNodesWithTagName(HTMLTags.paragraph, childNodes: domNodes); + return [element]; + } +} diff --git a/lib/src/plugins/html/encoder/parser/todo_list_node_parser.dart b/lib/src/plugins/html/encoder/parser/todo_list_node_parser.dart new file mode 100644 index 000000000..bae2dd9d9 --- /dev/null +++ b/lib/src/plugins/html/encoder/parser/todo_list_node_parser.dart @@ -0,0 +1,41 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:html/dom.dart' as dom; + +class HTMLTodoListNodeParser extends HTMLNodeParser { + const HTMLTodoListNodeParser(); + + @override + String get id => TodoListBlockKeys.type; + + @override + String transformNodeToHTMLString( + Node node, { + required List encodeParsers, + }) { + assert(node.type == TodoListBlockKeys.type); + + return toHTMLString( + transformNodeToDomNodes(node, encodeParsers: encodeParsers), + ); + } + + @override + List transformNodeToDomNodes( + Node node, { + required List encodeParsers, + }) { + final delta = node.delta ?? Delta(); + final domNodes = deltaHTMLEncoder.convert(delta); + final elementNode = dom.Element.html(''); + elementNode.attributes['checked'] = + node.attributes[TodoListBlockKeys.checked].toString(); + domNodes.add(elementNode); + domNodes.addAll( + processChildrenNodes(node.children, encodeParsers: encodeParsers), + ); + + final element = + wrapChildrenNodesWithTagName(HTMLTags.div, childNodes: domNodes); + return [element]; + } +} diff --git a/lib/src/plugins/html/html_document.dart b/lib/src/plugins/html/html_document.dart index bc7f72621..bb57c3280 100644 --- a/lib/src/plugins/html/html_document.dart +++ b/lib/src/plugins/html/html_document.dart @@ -6,22 +6,43 @@ 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 'encoder/parser/html_parser.dart'; + /// Converts a html to [Document]. Document htmlToDocument(String html) { return const AppFlowyEditorHTMLCodec().decode(html); } /// Converts a [Document] to html. -String documentToHTML(Document document) { - return const AppFlowyEditorHTMLCodec().encode(document); +String documentToHTML( + Document document, { + List customParsers = const [], +}) { + return AppFlowyEditorHTMLCodec( + encodeParsers: [ + ...customParsers, + const HTMLTextNodeParser(), + const HTMLBulletedListNodeParser(), + const HTMLNumberedListNodeParser(), + const HTMLTodoListNodeParser(), + const HTMLQuoteNodeParser(), + const HTMLHeadingNodeParser(), + const HTMLImageNodeParser(), + ], + ).encode(document); } class AppFlowyEditorHTMLCodec extends Codec { - const AppFlowyEditorHTMLCodec(); + const AppFlowyEditorHTMLCodec({ + this.encodeParsers = const [], + }); + + final List encodeParsers; @override Converter get decoder => DocumentHTMLDecoder(); @override - Converter get encoder => DocumentHTMLEncoder(); + Converter get encoder => + DocumentHTMLEncoder(encodeParsers: encodeParsers); } diff --git a/lib/src/plugins/html/html_document_decoder.dart b/lib/src/plugins/html/html_document_decoder.dart index 82c402915..46aa2d39b 100644 --- a/lib/src/plugins/html/html_document_decoder.dart +++ b/lib/src/plugins/html/html_document_decoder.dart @@ -1,8 +1,9 @@ import 'dart:collection'; import 'dart:convert'; + import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:html/parser.dart' show parse; import 'package:html/dom.dart' as dom; +import 'package:html/parser.dart' show parse; class DocumentHTMLDecoder extends Converter { DocumentHTMLDecoder(); @@ -202,20 +203,26 @@ class DocumentHTMLDecoder extends Converter { final delta = Delta(); final nodes = []; 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) { + if (child.children.isNotEmpty && + HTMLTags.formattingElements.contains(child.localName) == false) { + //rich editor for webs do this so handling that case for href demo + + nodes.addAll(_parseElement(child.children)); + } else { + if (HTMLTags.specialElements.contains(child.localName)) { nodes.addAll( _parseSpecialElements( - seocondChild, + child, type: ParagraphBlockKeys.type, ), ); + } else { + final attributes = _parserFormattingElementAttributes(child); + delta.insert(child.text, attributes: attributes); } - } else { - final attributes = _parserFormattingElementAttributes(child); - delta.insert(child.text, attributes: attributes); } } else { delta.insert(child.text ?? ''); diff --git a/lib/src/plugins/html/html_document_encoder.dart b/lib/src/plugins/html/html_document_encoder.dart index 658d02867..3a8049204 100644 --- a/lib/src/plugins/html/html_document_encoder.dart +++ b/lib/src/plugins/html/html_document_encoder.dart @@ -1,257 +1,28 @@ import 'dart:convert'; + import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:html/dom.dart' as dom; -import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; class DocumentHTMLEncoder extends Converter { - DocumentHTMLEncoder(); + DocumentHTMLEncoder({this.encodeParsers = const []}); + final List encodeParsers; - dom.Element? _stashListContainer; - final List _result = []; - final List nodes = []; @override String convert(Document input) { - List documentNodes = input.root.children.toList(); - nodes.addAll(documentNodes); - return toHTMLString(); - } - - List 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( - '', - (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 textDecoration = []; - if (attributes[BuiltInAttributeKey.strikethrough] == true) { - textDecoration.add('line-through'); - } - if (attributes[BuiltInAttributeKey.underline] == true) { - textDecoration.add('underline'); - } - - return textDecoration.join(' '); - } - - String _attributesToCssStyle(Map attributes) { - final cssMap = {}; - 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]), + final buffer = StringBuffer(); + for (final node in input.root.children) { + HTMLNodeParser? parser = encodeParsers.firstWhereOrNull( + (element) => element.id == node.type, ); - 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 cssMap) { - return cssMap.entries.map((e) => '${e.key}: ${e.value}').join('; '); - } - - dom.Element _deltaToHtml( - Delta delta, { - required String type, - required Attributes attributes, - required Iterable children, - }) { - final childNodes = []; - - String tagName = HTMLTags.paragraph; - - if (type == BulletedListBlockKeys.type || - type == NumberedListBlockKeys.type) { - tagName = HTMLTags.list; - } else if (type == TodoListBlockKeys.type) { - final node = dom.Element.html(''); - - 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; + if (parser != null) { + buffer.write( + parser.transformNodeToHTMLString( + node, + encodeParsers: encodeParsers, + ), + ); } - } 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(HTMLTag.span, childNodes: [anchor])); - } - } - } - - if (tagName == HTMLTags.blockQuote) { - return _insertText(HTMLTag.blockQuote, childNodes: childNodes); - } else if (tagName == HTMLTags.checkbox) { - return _insertText(HTMLTag.div, childNodes: childNodes); - } else if (!HTMLTags.isTopLevel(tagName)) { - return _insertText(HTMLTag.list, childNodes: childNodes); - } else { - return _insertText(tagName, childNodes: childNodes); - } - } - - dom.Element _applyAttributes(Attributes attributes, {required String text}) { - if (attributes[AppFlowyRichTextKeys.bold] == true) { - final strong = dom.Element.tag(HTMLTags.strong); - strong.append(dom.Text(text)); - return strong; - } else if (attributes[AppFlowyRichTextKeys.underline] == true) { - final underline = dom.Element.tag(HTMLTags.underline); - underline.append(dom.Text(text)); - return underline; - } else if (attributes[AppFlowyRichTextKeys.italic] == true) { - final italic = dom.Element.tag(HTMLTags.italic); - italic.append(dom.Text(text)); - return italic; - } else if (attributes[AppFlowyRichTextKeys.strikethrough] == true) { - final del = dom.Element.tag(HTMLTags.del); - del.append(dom.Text(text)); - return del; - } else if (attributes[AppFlowyRichTextKeys.code] == true) { - final code = dom.Element.tag(HTMLTags.code); - code.append(dom.Text(text)); - return code; - } else if (attributes[AppFlowyRichTextKeys.href] != null) { - final anchor = dom.Element.tag(HTMLTags.anchor); - anchor.attributes['href'] = attributes[AppFlowyRichTextKeys.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 childNodes, - }) { - final p = dom.Element.tag(tagName); - for (final node in childNodes) { - p.append(node); } - return p; + return buffer.toString(); } } diff --git a/lib/src/plugins/plugins.dart b/lib/src/plugins/plugins.dart index 29789d335..a26347b24 100644 --- a/lib/src/plugins/plugins.dart +++ b/lib/src/plugins/plugins.dart @@ -1,3 +1,5 @@ +export 'html/encoder/delta_html_encoder.dart'; +export 'html/encoder/parser/html_parser.dart'; export 'html/html_document.dart'; export 'html/html_document_decoder.dart'; export 'html/html_document_encoder.dart'; diff --git a/test/plugins/html/decoder/document_html_decoder_test.dart b/test/plugins/html/decoder/document_html_decoder_test.dart index 43d619684..cd41bf3d8 100644 --- a/test/plugins/html/decoder/document_html_decoder_test.dart +++ b/test/plugins/html/decoder/document_html_decoder_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() async { group('document_html_decoder_test.dart', () { @@ -14,7 +14,6 @@ void main() async { }); test('nested parser document', () async { final result = DocumentHTMLDecoder().convert(nestedHTML); - expect(result.toJson(), nestedDelta); }); }); @@ -294,7 +293,7 @@ const example = { } }; const nestedHTML = - '''

Welcome to the playground

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 @lexical/react. Try typing in some text with different formats.

\t

Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!

If you'd like to find out more about Lexical, you can:

Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.

'''; + '''

Welcome to the playground

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 @lexical/react. Try typing in some text with different formats.

Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!

If you'd like to find out more about Lexical, you can:

  • Playground code can be found here.
  • Playground code can be found here.

Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.

'''; const nestedDelta = { 'document': { 'type': 'page', @@ -323,7 +322,11 @@ const nestedDelta = { {'insert': '. Try typing in '}, { 'insert': 'some text', - 'attributes': {'bold': true} + 'attributes': { + 'bold': true, + "italic": true, + 'href': 'https://appflowy.io' + } }, {'insert': ' with '}, { @@ -334,14 +337,6 @@ const nestedDelta = { ] } }, - { - 'type': 'paragraph', - 'data': { - 'delta': [ - {'insert': '\t'} - ] - } - }, { 'type': 'image', 'data': { @@ -399,11 +394,7 @@ const nestedDelta = { } } ], - 'data': { - 'delta': [ - {'insert': '\t'} - ] - } + 'data': {'delta': []} }, { 'type': 'bulleted_list', diff --git a/test/plugins/html/delta_encoder_test.dart b/test/plugins/html/delta_encoder_test.dart new file mode 100644 index 000000000..055e690cd --- /dev/null +++ b/test/plugins/html/delta_encoder_test.dart @@ -0,0 +1,308 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:html/dom.dart' as dom; + +void main() async { + group('delta_html_encoder.dart', () { + test('bold', () { + final delta = Delta( + operations: [ + TextInsert('Welcome to '), + TextInsert( + 'AppFlowy', + attributes: { + BuiltInAttributeKey.bold: true, + }, + ), + ], + ); + final childNodes = []; + final result = DeltaHTMLEncoder().convert(delta); + + childNodes.add(dom.Text('Welcome to ')); + + final strong = dom.Element.tag(HTMLTags.strong); + strong.append(dom.Text('AppFlowy')); + childNodes.add(strong); + + expect( + result.first.text, + childNodes.first.text, + ); + final resultelement = result.last as dom.Element; + final expectElement = childNodes.last as dom.Element; + expect( + resultelement.className, + expectElement.className, + ); + }); + + test('italic', () { + final delta = Delta( + operations: [ + TextInsert('Welcome to '), + TextInsert( + 'AppFlowy', + attributes: { + BuiltInAttributeKey.italic: true, + }, + ), + ], + ); + final childNodes = []; + final result = DeltaHTMLEncoder().convert(delta); + + childNodes.add(dom.Text('Welcome to ')); + + final italic = dom.Element.tag(HTMLTags.italic); + italic.append(dom.Text('AppFlowy')); + childNodes.add(italic); + + expect( + result.first.text, + childNodes.first.text, + ); + final resultelement = result.last as dom.Element; + final expectElement = childNodes.last as dom.Element; + expect( + resultelement.className, + expectElement.className, + ); + }); + + test('underline', () { + final delta = Delta( + operations: [ + TextInsert('Welcome to '), + TextInsert( + 'AppFlowy', + attributes: { + BuiltInAttributeKey.underline: true, + BuiltInAttributeKey.bold: true, + BuiltInAttributeKey.italic: true, + BuiltInAttributeKey.code: true, + }, + ), + ], + ); + final childNodes = []; + final result = DeltaHTMLEncoder().convert(delta); + + childNodes.add(dom.Text('Welcome to ')); + + final underline = dom.Element.tag(HTMLTags.underline); + underline.append(dom.Text('AppFlowy')); + childNodes.add(underline); + + expect( + result.first.text, + childNodes.first.text, + ); + final resultelement = result.last as dom.Element; + final expectElement = childNodes.last as dom.Element; + expect( + resultelement.className, + expectElement.className, + ); + }); + + test('strikethrough', () { + final delta = Delta( + operations: [ + TextInsert('Welcome to '), + TextInsert( + 'AppFlowy', + attributes: { + BuiltInAttributeKey.strikethrough: true, + }, + ), + ], + ); + final childNodes = []; + final result = DeltaHTMLEncoder().convert(delta); + + childNodes.add(dom.Text('Welcome to ')); + + final del = dom.Element.tag(HTMLTags.del); + del.append(dom.Text('AppFlowy')); + childNodes.add(del); + + expect( + result.first.text, + childNodes.first.text, + ); + final resultelement = result.last as dom.Element; + final expectElement = childNodes.last as dom.Element; + expect( + resultelement.className, + expectElement.className, + ); + }); + + test('href', () { + final delta = Delta( + operations: [ + TextInsert('Welcome to '), + TextInsert( + 'AppFlowy', + attributes: { + BuiltInAttributeKey.href: 'https://appflowy.io', + }, + ), + ], + ); + final childNodes = []; + final result = DeltaHTMLEncoder().convert(delta); + + childNodes.add(dom.Text('Welcome to ')); + + final anchor = dom.Element.tag(HTMLTags.anchor); + anchor.attributes['href'] = "https://appflowy.io"; + anchor.append(dom.Text('AppFlowy')); + + childNodes.add(anchor); + + expect( + result.first.text, + childNodes.first.text, + ); + final resultelement = result.last as dom.Element; + final expectElement = childNodes.last as dom.Element; + expect( + resultelement.className, + expectElement.className, + ); + }); + + test('code', () { + final delta = Delta( + operations: [ + TextInsert('Welcome to '), + TextInsert( + 'AppFlowy', + attributes: { + BuiltInAttributeKey.code: true, + }, + ), + ], + ); + final childNodes = []; + final result = DeltaHTMLEncoder().convert(delta); + + childNodes.add(dom.Text('Welcome to ')); + + final code = dom.Element.tag(HTMLTags.code); + + code.append(dom.Text('AppFlowy')); + + childNodes.add(code); + + expect( + result.first.text, + childNodes.first.text, + ); + final resultelement = result.last as dom.Element; + final expectElement = childNodes.last as dom.Element; + expect( + resultelement.className, + expectElement.className, + ); + }); + + test('composition', () { + final delta = Delta( + operations: [ + TextInsert( + 'Welcome', + attributes: { + BuiltInAttributeKey.code: true, + BuiltInAttributeKey.italic: true, + BuiltInAttributeKey.bold: true, + BuiltInAttributeKey.underline: true, + }, + ), + TextInsert(' '), + TextInsert( + 'to', + attributes: { + BuiltInAttributeKey.italic: true, + BuiltInAttributeKey.bold: true, + BuiltInAttributeKey.strikethrough: true, + }, + ), + TextInsert(' '), + TextInsert( + 'AppFlowy', + attributes: { + BuiltInAttributeKey.href: 'https://appflowy.io', + BuiltInAttributeKey.bold: true, + BuiltInAttributeKey.italic: true, + BuiltInAttributeKey.underline: true + }, + ), + ], + ); + final result = DeltaHTMLEncoder().convert(delta); + + expect( + result.first.attributes.toString(), + '''{style: font-weight: bold; text-decoration: underline; font-style: italic}''', + ); + expect( + result.first.text, + "Welcome", + ); + + expect( + result[2].attributes.toString(), + '''{style: font-weight: bold; text-decoration: line-through; font-style: italic}''', + ); + expect( + result[2].text, + "to", + ); + + expect( + result[4].text, + "AppFlowy", + ); + final element = result[4] as dom.Element; + expect( + element.localName, + "a", + ); + + expect( + element.children.length, + 1, + ); + final anchorChildElement = element.children.first; + expect( + anchorChildElement.localName, + "u", + ); + expect( + element.children.first.children.length, + 1, + ); + expect( + element.children.first.children.first.localName, + "i", + ); + + expect( + element.children.first.children.first.children.length, + 1, + ); + expect( + element.children.first.children.first.children.first.localName, + "strong", + ); + + expect( + element.children.first.children.first.children.first.text, + "AppFlowy", + ); + }); + }); +} diff --git a/test/plugins/html/encoder/document_html_encoder_test.dart b/test/plugins/html/encoder/document_html_encoder_test.dart index 0e34348d9..2a6402b2c 100644 --- a/test/plugins/html/encoder/document_html_encoder_test.dart +++ b/test/plugins/html/encoder/document_html_encoder_test.dart @@ -1,27 +1,39 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() async { + List parser = [ + const HTMLTextNodeParser(), + const HTMLBulletedListNodeParser(), + const HTMLNumberedListNodeParser(), + const HTMLTodoListNodeParser(), + const HTMLQuoteNodeParser(), + const HTMLHeadingNodeParser(), + const HTMLImageNodeParser(), + ]; group('document_html_encoder_test.dart', () { setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); }); test('parser document', () async { - final result = DocumentHTMLEncoder().convert(Document.fromJson(delta)); + final result = DocumentHTMLEncoder( + encodeParsers: parser, + ).convert(Document.fromJson(delta)); expect(result, example); }); test('nested parser document', () async { - final result = - DocumentHTMLEncoder().convert(Document.fromJson(nestedDelta)); + final result = DocumentHTMLEncoder( + encodeParsers: parser, + ).convert(Document.fromJson(nestedDelta)); - expect(result, nestedhtml); + expect(result, nestedHTML); }); }); } const example = - '''

AppFlowyEditor

👋 Welcome to AppFlowy Editor

AppFlowy Editor is a highly customizable rich-text editor

Here is an example your you can give a try

Span element

Span element two

Span element three

This is an anchor tag!

Features!

  • [x] Customizable
  • [x] Test-covered
  • [ ] more to come!
  • First item
  • Second item
  • List element
This is a quote!

Code block

Italic one

Italic two

Bold tag

You can also use AppFlowy Editor as a component to build your own app.

Awesome features

If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!

'''; + '''

AppFlowyEditor

👋 Welcome to AppFlowy Editor

AppFlowy Editor is a highly customizable rich-text editor

Here is an example your you can give a try

Span element

Span element two

Span element three

This is an anchor tag!

Features!

  • [x] Customizable
  • [x] Test-covered
  • [ ] more to come!
  • First item
  • Second item
  • List element
This is a quote!

Code block

Italic one

Italic two

Bold tag

You can also use AppFlowy Editor as a component to build your own app.

Awesome features

If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!

'''; const delta = { 'document': { @@ -293,8 +305,8 @@ const delta = { ] } }; -const nestedhtml = - '''

Welcome to the playground

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 @lexical/react. Try typing in some text with different formats.

\t

Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!

If you'd like to find out more about Lexical, you can:

Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.

'''; +const nestedHTML = + '''

Welcome to the playground

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 @lexical/react. Try typing in some text with different formats.

Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!

If you'd like to find out more about Lexical, you can:

  • Playground code can be found here.
  • Playground code can be found here.

Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.

'''; const nestedDelta = { 'document': { 'type': 'page', @@ -323,7 +335,11 @@ const nestedDelta = { {'insert': '. Try typing in '}, { 'insert': 'some text', - 'attributes': {'bold': true} + 'attributes': { + 'bold': true, + "italic": true, + 'href': 'https://appflowy.io' + } }, {'insert': ' with '}, { @@ -334,21 +350,11 @@ const nestedDelta = { ] } }, - { - 'type': 'paragraph', - 'data': { - 'delta': [ - {'insert': '\t'} - ] - } - }, { 'type': 'image', 'data': { 'url': 'https://richtexteditor.com/images/editor-image.png', 'align': 'center', - 'height': null, - 'width': null, } }, { @@ -398,16 +404,10 @@ const nestedDelta = { 'data': { 'url': 'https://richtexteditor.com/images/editor-image.png', 'align': 'center', - 'height': null, - 'width': null, } } ], - 'data': { - 'delta': [ - {'insert': '\t'} - ] - } + 'data': {'delta': []} }, { 'type': 'bulleted_list', diff --git a/test/plugins/html/encoder/parser/image_node_parser_test.dart b/test/plugins/html/encoder/parser/image_node_parser_test.dart new file mode 100644 index 000000000..6f2d9e447 --- /dev/null +++ b/test/plugins/html/encoder/parser/image_node_parser_test.dart @@ -0,0 +1,34 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + List parser = [ + const HTMLTextNodeParser(), + const HTMLBulletedListNodeParser(), + const HTMLNumberedListNodeParser(), + const HTMLTodoListNodeParser(), + const HTMLQuoteNodeParser(), + const HTMLHeadingNodeParser(), + const HTMLImageNodeParser(), + ]; + group('html_image_node_parser.dart', () { + test('parser image node', () { + final node = Node( + type: 'image', + attributes: { + 'url': 'https://appflowy.io', + }, + ); + + final result = const HTMLImageNodeParser() + .transformNodeToHTMLString(node, encodeParsers: parser); + + expect(result, ''); + }); + + test('ImageNodeParser id getter', () { + const imageNodeParser = ImageNodeParser(); + expect(imageNodeParser.id, 'image'); + }); + }); +} diff --git a/test/plugins/html/encoder/parser/text_node_parser_test.dart b/test/plugins/html/encoder/parser/text_node_parser_test.dart new file mode 100644 index 000000000..e55a1ac31 --- /dev/null +++ b/test/plugins/html/encoder/parser/text_node_parser_test.dart @@ -0,0 +1,117 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + List parser = [ + const HTMLTextNodeParser(), + const HTMLBulletedListNodeParser(), + const HTMLNumberedListNodeParser(), + const HTMLTodoListNodeParser(), + const HTMLQuoteNodeParser(), + const HTMLHeadingNodeParser(), + const HTMLImageNodeParser(), + ]; + group('html_text_node_parser.dart', () { + const text = 'Welcome to AppFlowy'; + + test('heading style', () { + for (var i = 1; i <= 6; i++) { + final node = headingNode( + level: i, + attributes: { + 'delta': (Delta()..insert(text)).toJson(), + }, + ); + expect( + const HTMLHeadingNodeParser() + .transformNodeToHTMLString(node, encodeParsers: parser), + 'Welcome to AppFlowy', + ); + } + }); + + test('bulleted list style', () { + final node = bulletedListNode( + attributes: { + 'delta': (Delta()..insert(text)).toJson(), + }, + ); + expect( + const HTMLBulletedListNodeParser() + .transformNodeToHTMLString(node, encodeParsers: parser), + '
  • Welcome to AppFlowy
', + ); + }); + + test('numbered list style', () { + final node = numberedListNode( + attributes: { + 'delta': (Delta()..insert(text)).toJson(), + }, + ); + expect( + const HTMLNumberedListNodeParser() + .transformNodeToHTMLString(node, encodeParsers: parser), + '
  1. Welcome to AppFlowy
', + ); + }); + + test('todo list style', () { + final checkedNode = todoListNode( + checked: true, + attributes: { + 'delta': (Delta()..insert(text)).toJson(), + }, + ); + final uncheckedNode = todoListNode( + checked: false, + attributes: { + 'delta': (Delta()..insert(text)).toJson(), + }, + ); + expect( + const HTMLTodoListNodeParser() + .transformNodeToHTMLString(checkedNode, encodeParsers: parser), + '
Welcome to AppFlowy
', + ); + expect( + const HTMLTodoListNodeParser() + .transformNodeToHTMLString(uncheckedNode, encodeParsers: parser), + '
Welcome to AppFlowy
', + ); + }); + + test('quote style', () { + final node = quoteNode( + attributes: { + 'delta': (Delta()..insert(text)).toJson(), + }, + ); + expect( + const HTMLQuoteNodeParser() + .transformNodeToHTMLString(node, encodeParsers: parser), + '
Welcome to AppFlowy
', + ); + }); + + test('fallback', () { + final node = paragraphNode( + attributes: { + 'delta': (Delta() + ..insert( + text, + attributes: { + 'bold': true, + }, + )) + .toJson(), + }, + ); + expect( + const HTMLTextNodeParser() + .transformNodeToHTMLString(node, encodeParsers: parser), + "

Welcome to AppFlowy

", + ); + }); + }); +} diff --git a/test/plugins/html/html_document_test.dart b/test/plugins/html/html_document_test.dart index 6a2de5608..10c4de99b 100644 --- a/test/plugins/html/html_document_test.dart +++ b/test/plugins/html/html_document_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('html_document_test.dart tests', () { @@ -8,20 +8,20 @@ void main() { expect(document.toJson(), data); }); test('nestedhtmlToDocument()', () { - final document = htmlToDocument(nestedhtml); + final document = htmlToDocument(nestedHTML); expect(document.toJson(), nestedDelta); }); }); group('document_html_test.dart tests', () { - test('documentToHtml()', () { + test('documentToHTML()', () { final document = documentToHTML(Document.fromJson(data)); expect(document, rawHTML); }); - test('nesteddocumentToHtml()', () { + test('nesteddocumentToHTML()', () { final document = documentToHTML(Document.fromJson(nestedDelta)); - expect(document, nestedhtml); + expect(document, nestedHTML); }); }); } @@ -297,10 +297,10 @@ const data = { } }; const rawHTML = - '''

AppFlowyEditor

👋 Welcome to AppFlowy Editor

AppFlowy Editor is a highly customizable rich-text editor

Here is an example your you can give a try

Span element

Span element two

Span element three

This is an anchor tag!

Features!

  • [x] Customizable
  • [x] Test-covered
  • [ ] more to come!
  • First item
  • Second item
  • List element
This is a quote!

Code block

Italic one

Italic two

Bold tag

You can also use AppFlowy Editor as a component to build your own app.

Awesome features

If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!

'''; + '''

AppFlowyEditor

👋 Welcome to AppFlowy Editor

AppFlowy Editor is a highly customizable rich-text editor

Here is an example your you can give a try

Span element

Span element two

Span element three

This is an anchor tag!

Features!

  • [x] Customizable
  • [x] Test-covered
  • [ ] more to come!
  • First item
  • Second item
  • List element
This is a quote!

Code block

Italic one

Italic two

Bold tag

You can also use AppFlowy Editor as a component to build your own app.

Awesome features

If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!

'''; -const nestedhtml = - '''

Welcome to the playground

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 @lexical/react. Try typing in some text with different formats.

\t

Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!

If you'd like to find out more about Lexical, you can:

Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.

'''; +const nestedHTML = + '''

Welcome to the playground

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 @lexical/react. Try typing in some text with different formats.

Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!

If you'd like to find out more about Lexical, you can:

  • Playground code can be found here.
  • Playground code can be found here.

Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance 🙂.

'''; const nestedDelta = { 'document': { 'type': 'page', @@ -329,7 +329,11 @@ const nestedDelta = { {'insert': '. Try typing in '}, { 'insert': 'some text', - 'attributes': {'bold': true} + 'attributes': { + 'bold': true, + "italic": true, + 'href': 'https://appflowy.io' + } }, {'insert': ' with '}, { @@ -340,19 +344,11 @@ const nestedDelta = { ] } }, - { - 'type': 'paragraph', - 'data': { - 'delta': [ - {'insert': '\t'} - ] - } - }, { 'type': 'image', 'data': { 'url': 'https://richtexteditor.com/images/editor-image.png', - 'align': 'center' + 'align': 'center', } }, { @@ -401,15 +397,11 @@ const nestedDelta = { 'type': 'image', 'data': { 'url': 'https://richtexteditor.com/images/editor-image.png', - 'align': 'center' + 'align': 'center', } } ], - 'data': { - 'delta': [ - {'insert': '\t'} - ] - } + 'data': {'delta': []} }, { 'type': 'bulleted_list',