From 38477015ce6997ae00c1ef3e07ac2996a69f343e Mon Sep 17 00:00:00 2001 From: alihassan143 Date: Fri, 9 Jun 2023 17:32:34 +0500 Subject: [PATCH] fix: nested elements getting ignored --- .../heading_block_component.dart | 2 + .../plugins/html/html_document_decoder.dart | 52 +++-- .../plugins/html/html_document_encoder.dart | 30 ++- .../decoder/document_html_decoder_test.dart | 199 ++++++++++++++++- .../encoder/document_html_encoder_test.dart | 200 ++++++++++++++++- test/plugins/html/html_document_test.dart | 206 +++++++++++++++++- 6 files changed, 665 insertions(+), 24 deletions(-) diff --git a/lib/src/editor/block_component/heading_block_component/heading_block_component.dart b/lib/src/editor/block_component/heading_block_component/heading_block_component.dart index 8a13d4553..ccb872556 100644 --- a/lib/src/editor/block_component/heading_block_component/heading_block_component.dart +++ b/lib/src/editor/block_component/heading_block_component/heading_block_component.dart @@ -22,10 +22,12 @@ Node headingNode({ required int level, Delta? delta, Attributes? attributes, + Iterable? children, }) { attributes ??= {'delta': (delta ?? Delta()).toJson()}; return Node( type: HeadingBlockKeys.type, + children: children ?? [], attributes: { HeadingBlockKeys.level: level, ...attributes, diff --git a/lib/src/plugins/html/html_document_decoder.dart b/lib/src/plugins/html/html_document_decoder.dart index ede49149f..63d9d1208 100644 --- a/lib/src/plugins/html/html_document_decoder.dart +++ b/lib/src/plugins/html/html_document_decoder.dart @@ -79,18 +79,21 @@ class DocumentHTMLDecoder extends Converter { ) ]; case HTMLTags.paragraph: - return [_parseParagraphElement(element)]; + return _parseParagraphElement(element); case HTMLTags.blockQuote: return _parseBlockQuoteElement(element); case HTMLTags.image: return [_parseImageElement(element)]; default: - return [paragraphNode(text: element.text)]; + return _parseParagraphElement(element); } } - Attributes _parserFormattingElementAttributes(dom.Element element) { + Attributes _parserFormattingElementAttributes( + dom.Element element, + ) { final localName = element.localName; + Attributes attributes = {}; switch (localName) { case HTMLTags.bold || HTMLTags.strong: @@ -133,17 +136,23 @@ class DocumentHTMLDecoder extends Converter { dom.Element element, { required int level, }) { - final delta = _parseDeltaElement(element); + final (delta, specialNodes) = _parseDeltaElement(element); return headingNode( level: level, + children: specialNodes, delta: delta, ); } Iterable _parseBlockQuoteElement(dom.Element element) { - return element.children - .map((child) => _parseListElement(child, type: QuoteBlockKeys.type)) - .toList(); + final (delta, nodes) = _parseDeltaElement(element); + return [ + Node( + type: QuoteBlockKeys.type, + children: nodes, + attributes: {ParagraphBlockKeys.delta: delta.toJson()}, + ) + ]; } Iterable _parseUnOrderListElement(dom.Element element) { @@ -166,16 +175,17 @@ class DocumentHTMLDecoder extends Converter { dom.Element element, { required String type, }) { - final delta = _parseDeltaElement(element); + final (delta, node) = _parseDeltaElement(element); return Node( type: type != ParagraphBlockKeys.type ? type : BulletedListBlockKeys.type, + children: node, attributes: {ParagraphBlockKeys.delta: delta.toJson()}, ); } - Node _parseParagraphElement(dom.Element element) { - final delta = _parseDeltaElement(element); - return paragraphNode(delta: delta); + Iterable _parseParagraphElement(dom.Element element) { + final (delta, specialNodes) = _parseDeltaElement(element); + return [paragraphNode(delta: delta), ...specialNodes]; } Node _parseImageElement(dom.Element element) { @@ -185,18 +195,30 @@ class DocumentHTMLDecoder extends Converter { ); } - Delta _parseDeltaElement(dom.Element element) { + (Delta, Iterable) _parseDeltaElement(dom.Element element) { final delta = Delta(); + final nodes = []; final children = element.nodes.toList(); for (final child in children) { if (child is dom.Element) { - final attributes = _parserFormattingElementAttributes(child); - delta.insert(child.text, attributes: attributes); + 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; + return (delta, nodes); } Attributes? _getDeltaAttributesFromHTMLAttributes( diff --git a/lib/src/plugins/html/html_document_encoder.dart b/lib/src/plugins/html/html_document_encoder.dart index f9bddbd71..4e88eb7b9 100644 --- a/lib/src/plugins/html/html_document_encoder.dart +++ b/lib/src/plugins/html/html_document_encoder.dart @@ -75,6 +75,7 @@ class DocumentHTMLEncoder extends Converter { return _deltaToHtml( Delta.fromJson(documentNode.attributes[ParagraphBlockKeys.delta]), type: documentNode.type, + children: documentNode.children, attributes: documentNode.attributes, ); } @@ -129,8 +130,10 @@ class DocumentHTMLEncoder extends Converter { Delta delta, { required String type, required Attributes attributes, + required Iterable children, }) { final childNodes = []; + String tagName = HTMLTags.paragraph; if (type == BulletedListBlockKeys.type || @@ -176,17 +179,32 @@ class DocumentHTMLEncoder extends Converter { } } } + 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) { - final blockQuote = dom.Element.tag(tagName); - blockQuote.append(_insertText(HTMLTag.paragraph, childNodes: childNodes)); - return blockQuote; + return _insertText(HTMLTag.blockQuote, childNodes: childNodes); } else if (tagName == HTMLTags.checkbox) { return _insertText(HTMLTag.div, childNodes: childNodes); } else if (!HTMLTags.isTopLevel(tagName)) { - final result = dom.Element.tag(HTMLTags.list); - result.append(_insertText(HTMLTag.paragraph, childNodes: childNodes)); - return result; + return _insertText(HTMLTag.list, childNodes: childNodes); } else { return _insertText(tagName, childNodes: childNodes); } diff --git a/test/plugins/html/decoder/document_html_decoder_test.dart b/test/plugins/html/decoder/document_html_decoder_test.dart index 52489d598..0e1a9831d 100644 --- a/test/plugins/html/decoder/document_html_decoder_test.dart +++ b/test/plugins/html/decoder/document_html_decoder_test.dart @@ -13,11 +13,16 @@ void main() async { expect(result.toJson(), example); }); + test('nested parser document', () async { + final result = DocumentHTMLDecoder().convert(nestedhtml); + + expect(result.toJson(), nestedDelta); + }); }); } 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 example = { 'document': { @@ -289,3 +294,195 @@ 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 🙂.

'''; +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': []} + } + ] + } +}; diff --git a/test/plugins/html/encoder/document_html_encoder_test.dart b/test/plugins/html/encoder/document_html_encoder_test.dart index b0ee33054..3855f53d4 100644 --- a/test/plugins/html/encoder/document_html_encoder_test.dart +++ b/test/plugins/html/encoder/document_html_encoder_test.dart @@ -12,11 +12,17 @@ void main() async { expect(result, example); }); + test('nested parser document', () async { + final result = + DocumentHTMLEncoder().convert(Document.fromJson(nestedDelta)); + + 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': { @@ -288,3 +294,195 @@ 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 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': []} + } + ] + } +}; diff --git a/test/plugins/html/html_document_test.dart b/test/plugins/html/html_document_test.dart index 2a055fd3e..e0495d9fa 100644 --- a/test/plugins/html/html_document_test.dart +++ b/test/plugins/html/html_document_test.dart @@ -1,5 +1,6 @@ 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', () { @@ -7,12 +8,22 @@ void main() { final document = htmlToDocument(rawHTML); expect(document.toJson(), data); }); + test('nestedhtmlToDocument()', () { + final document = htmlToDocument(nestedhtml); + expect(document.toJson(), nestedDelta); + }); }); group('document_html_test.dart tests', () { test('documentToHtml()', () { final document = documentToHTML(Document.fromJson(data)); + expect(document, rawHTML); }); + test('nesteddocumentToHtml()', () { + final document = documentToHTML(Document.fromJson(nestedDelta)); + + expect(document, nestedhtml); + }); }); } @@ -287,4 +298,197 @@ 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 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': []} + } + ] + } +};