Skip to content

Commit

Permalink
feat: html encode parser added (#314)
Browse files Browse the repository at this point in the history
  • Loading branch information
alihassan143 authored Jul 20, 2023
1 parent 8a76e36 commit 3ee7e31
Show file tree
Hide file tree
Showing 21 changed files with 1,069 additions and 326 deletions.
6 changes: 3 additions & 3 deletions lib/src/infra/html_converter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class HTMLToNodesConverter {
return result;
}

Attributes? _getDeltaAttributesFromHtmlAttributes(
Attributes? _getDeltaAttributesFromHTMLAttributes(
LinkedHashMap<Object, String> htmlAttributes,
) {
final attrs = <String, dynamic>{};
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -542,7 +542,7 @@ class NodesToHTMLConverter {
/// ```html
/// <span style='...'>Text</span>
/// ```
// html.Element _deltaToHtml(
// html.Element _deltaToHTML(
// Delta delta, {
// String? subType,
// String? heading,
Expand Down
152 changes: 152 additions & 0 deletions lib/src/plugins/html/encoder/delta_html_encoder.dart
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 lib/src/plugins/html/encoder/parser/bulleted_list_node_parser.dart
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 lib/src/plugins/html/encoder/parser/heading_node_parser.dart
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];
}
}
57 changes: 57 additions & 0 deletions lib/src/plugins/html/encoder/parser/html_node_parser.dart
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', '');
}
8 changes: 8 additions & 0 deletions lib/src/plugins/html/encoder/parser/html_parser.dart
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 lib/src/plugins/html/encoder/parser/image_node_parser.dart
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,
),
];
}
}
Loading

0 comments on commit 3ee7e31

Please sign in to comment.