-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: html encode parser added (#314)
- Loading branch information
1 parent
8a76e36
commit 3ee7e31
Showing
21 changed files
with
1,069 additions
and
326 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Delta, List<dom.Node>> { | ||
@override | ||
List<dom.Node> convert(Delta input) { | ||
return input | ||
.whereType<TextInsert>() | ||
.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 <a href="https://www.google.com" rel="noopener noreferrer" target="_blank"><strong><em><u>demo</u></em></strong></a> | ||
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<String, dynamic> attributes) { | ||
final cssMap = <String, String>{}; | ||
|
||
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('; '); | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
lib/src/plugins/html/encoder/parser/bulleted_list_node_parser.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HTMLNodeParser> encodeParsers, | ||
}) { | ||
assert(node.type == BulletedListBlockKeys.type); | ||
|
||
return toHTMLString( | ||
transformNodeToDomNodes(node, encodeParsers: encodeParsers), | ||
); | ||
} | ||
|
||
@override | ||
List<dom.Node> transformNodeToDomNodes( | ||
Node node, { | ||
required List<HTMLNodeParser> 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), | ||
]; | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
lib/src/plugins/html/encoder/parser/heading_node_parser.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HTMLNodeParser> encodeParsers, | ||
}) { | ||
return toHTMLString( | ||
transformNodeToDomNodes(node, encodeParsers: encodeParsers), | ||
); | ||
} | ||
|
||
@override | ||
List<dom.Node> transformNodeToDomNodes( | ||
Node node, { | ||
required List<HTMLNodeParser> 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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HTMLNodeParser> encodeParsers, | ||
}); | ||
|
||
/// Convert the [node] to html nodes. | ||
List<dom.Node> transformNodeToDomNodes( | ||
Node node, { | ||
required List<HTMLNodeParser> encodeParsers, | ||
}); | ||
|
||
dom.Element wrapChildrenNodesWithTagName( | ||
String tagName, { | ||
required List<dom.Node> childNodes, | ||
}) { | ||
final p = dom.Element.tag(tagName); | ||
for (final node in childNodes) { | ||
p.append(node); | ||
} | ||
return p; | ||
} | ||
|
||
// iterate over its children if exist | ||
List<dom.Node> processChildrenNodes( | ||
Iterable<Node> nodes, { | ||
required List<HTMLNodeParser> encodeParsers, | ||
}) { | ||
final result = <dom.Node>[]; | ||
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<dom.Node> nodes) => | ||
nodes.map((e) => stringify(e)).join().replaceAll('\n', ''); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; |
51 changes: 51 additions & 0 deletions
51
lib/src/plugins/html/encoder/parser/image_node_parser.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HTMLNodeParser> encodeParsers, | ||
}) { | ||
return toHTMLString( | ||
transformNodeToDomNodes(node, encodeParsers: encodeParsers), | ||
); | ||
} | ||
|
||
@override | ||
List<dom.Node> transformNodeToDomNodes( | ||
Node node, { | ||
required List<HTMLNodeParser> 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, | ||
), | ||
]; | ||
} | ||
} |
Oops, something went wrong.