fix: convert html to document (#152)

* fix:html to node converter fixes

* html tests added

* html decoder optimization

* feat: refactor code

* mobile platforms added

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Ali Hassan 2023-06-01 12:20:42 +05:00 • committed by GitHub
commit a77b25aa50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 751 additions and 47 deletions

View file

@ -30,9 +30,15 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
if (domNode is dom.Element) {
final localName = domNode.localName;
if (HTMLTags.formattingElements.contains(localName)) {
_parseFormattingElement(delta, domNode);
final attributes = _parserFormattingElementAttributes(domNode);
delta.insert(domNode.text, attributes: attributes);
} else if (HTMLTags.specialElements.contains(localName)) {
nodes.addAll(_parseSpecialElements(domNode));
nodes.addAll(
_parseSpecialElements(
domNode,
type: ParagraphBlockKeys.type,
),
);
}
} else if (domNode is dom.Text) {
delta.insert(domNode.text);
@ -46,7 +52,10 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
return nodes;
}
Iterable<Node> _parseSpecialElements(dom.Element element) {
Iterable<Node> _parseSpecialElements(
dom.Element element, {
required String type,
}) {
final localName = element.localName;
switch (localName) {
case HTMLTags.h1:
@ -60,22 +69,21 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
case HTMLTags.orderedList:
return _parseOrderListElement(element);
case HTMLTags.list:
return _parseListElement(element);
return _parseListElement(element, type: type);
case HTMLTags.paragraph:
return [_parseParagraphElement(element)];
case HTMLTags.blockQuote:
return [_parseBlockQuoteElement(element)];
case HTMLTags.image:
break;
return [_parseImageElement(element)];
default:
return [paragraphNode(text: element.text)];
}
return [];
}
void _parseFormattingElement(Delta delta, dom.Element element) {
Attributes _parserFormattingElementAttributes(dom.Element element) {
final localName = element.localName;
Attributes? attributes;
Attributes attributes = {};
switch (localName) {
case HTMLTags.bold || HTMLTags.strong:
attributes = {FlowyRichTextKeys.bold: true};
@ -92,81 +100,93 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
case HTMLTags.code:
attributes = {FlowyRichTextKeys.code: true};
case HTMLTags.span:
attributes = _getDeltaAttributesFromHTMLAttributes(
element.attributes,
);
final deltaAttributes = _getDeltaAttributesFromHTMLAttributes(
element.attributes,
) ??
{};
attributes.addAll(deltaAttributes);
break;
case HTMLTags.anchor:
final href = element.attributes['href'];
if (href != null) {
attributes = {
FlowyRichTextKeys.href: href,
};
attributes = {FlowyRichTextKeys.href: href};
}
break;
default:
assert(false, 'Unknown formatting element: $element');
break;
}
delta.insert(element.text, attributes: attributes);
for (final child in element.children) {
attributes.addAll(_parserFormattingElementAttributes(child));
}
return attributes;
}
Node _parseHeadingElement(
dom.Element element, {
required int level,
}) =>
headingNode(
level: level,
delta: Delta()..insert(element.text),
);
}) {
final delta = _parseDeltaElement(element);
return headingNode(
level: level,
delta: delta,
);
}
Node _parseBlockQuoteElement(dom.Element element) => quoteNode(
delta: Delta()..insert(element.text),
);
Iterable<Node> _parseUnOrderListElement(dom.Element element) {
final children = element.nodes.toList().whereType<dom.Element>();
return children.map(
(e) => bulletedListNode(
delta: Delta()
..insert(
e.text,
),
),
);
final result = <Node>[];
for (final child in element.children) {
result.addAll(_parseListElement(child, type: NumberedListBlockKeys.type));
}
return result;
}
Iterable<Node> _parseOrderListElement(dom.Element element) {
final children = element.nodes.toList().whereType<dom.Element>();
return children.map(
(e) => numberedListNode(
delta: Delta()
..insert(
e.text,
),
),
);
final result = <Node>[];
for (final child in element.children) {
result.addAll(_parseListElement(child, type: NumberedListBlockKeys.type));
}
return result;
}
Iterable<Node> _parseListElement(dom.Element element) {
final children = element.nodes.toList().whereType<dom.Element>();
return children
.map((e) => _parseSpecialElements(e))
.expand((element) => element);
Iterable<Node> _parseListElement(
dom.Element element, {
required String type,
}) {
final delta = _parseDeltaElement(element);
return [
Node(type: type, attributes: {ParagraphBlockKeys.delta: delta.toJson()})
];
}
Node _parseParagraphElement(dom.Element element) {
// TODO: parse image and checkbox.
final delta = _parseDeltaElement(element);
return paragraphNode(delta: delta);
}
Node _parseImageElement(dom.Element element) {
final src = element.attributes['src'] ?? '';
return imageNode(
url: src,
);
}
Delta _parseDeltaElement(dom.Element element) {
final delta = Delta();
final children = element.nodes.toList();
for (final child in children) {
if (child is dom.Element) {
_parseFormattingElement(delta, child);
final attributes = _parserFormattingElementAttributes(child);
delta.insert(child.text, attributes: attributes);
} else {
delta.insert(child.text ?? '');
}
}
return paragraphNode(delta: delta);
return delta;
}
Attributes? _getDeltaAttributesFromHTMLAttributes(

View file

@ -4,10 +4,12 @@ version: 1.0.0-dev.0
homepage: https://github.com/AppFlowy-IO/appflowy-editor
platforms:
android:
ios:
linux:
macos:
windows:
web:
windows:
environment:
sdk: ">=3.0.0 <4.0.0"

View file

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

View file

@ -0,0 +1,338 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
void main() {
group('html_document_test.dart tests', () {
test('htmlToDocument()', () {
final document = htmlToDocument(rawHTML);
expect(document.toJson(), data);
});
});
}
const data = {
'document': {
'type': 'page',
'children': [
{
'type': 'heading',
'data': {
'level': 1,
'delta': [
{'insert': 'AppFlowyEditor'}
]
}
},
{
'type': 'heading',
'data': {
'level': 2,
'delta': [
{'insert': '👋 '},
{
'insert': 'Welcome to',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'AppFlowy Editor',
'attributes': {
'bold': true,
'italic': true,
'href': 'appflowy.io'
}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'AppFlowy Editor is a '},
{
'insert': 'highly customizable',
'attributes': {'bold': true}
},
{'insert': ' '},
{
'insert': 'rich-text editor',
'attributes': {'italic': true}
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert': 'Here',
'attributes': {'underline': true}
},
{'insert': ' is an example '},
{
'insert': 'your',
'attributes': {'strikethrough': true}
},
{'insert': ' you can give a try'}
]
}
},
{
'type': 'heading',
'data': {
'level': 3,
'delta': [
{'insert': 'Features!'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': '[x] Customizable'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': '[x] Test-covered'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': '[ ] more to come!'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': 'First item'}
]
}
},
{
'type': 'numbered_list',
'data': {
'delta': [
{'insert': 'Second item'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'List element'}
]
}
},
{
'type': 'quote',
'data': {
'delta': [
{
'insert': '\n'
' This is a quote!\n'
''
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': 'You can also use '},
{
'insert': 'AppFlowy Editor',
'attributes': {'bold': true, 'italic': true}
},
{'insert': ' as a component to build your own app.'}
]
}
},
{
'type': 'heading',
'data': {
'level': 3,
'delta': [
{'insert': 'Awesome features'}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert':
'If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!'
}
]
}
},
{
'type': 'paragraph',
'data': {
'delta': [
{
'insert': '\n'
'\n'
' \n'
'\n'
'\n'
'\n'
'\n'
'\n'
''
},
{
'insert': 'Span element',
'attributes': {'bold': true, 'italic': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Span element two',
'attributes': {'underline': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Span element three',
'attributes': {'bold': true, 'strikethrough': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'This is an anchor tag!',
'attributes': {'href': 'https://appflowy.io'}
},
{
'insert': '\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
'\n'
''
},
{
'insert': '\n'
' Code block\n'
'',
'attributes': {'code': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Italic one',
'attributes': {'italic': true}
},
{'insert': ' '},
{
'insert': 'Italic two',
'attributes': {'italic': true}
},
{
'insert': '\n'
'\n'
''
},
{
'insert': 'Bold tag',
'attributes': {'bold': true}
},
{
'insert': '\n'
'\n'
'\n'
'\n'
'\n'
''
}
]
}
}
]
}
};
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>""";