|
@@ -1,33 +1,42 @@
|
|
import 'dart:collection';
|
|
import 'dart:collection';
|
|
|
|
+import 'dart:ui';
|
|
|
|
|
|
import 'package:flowy_editor/src/document/attributes.dart';
|
|
import 'package:flowy_editor/src/document/attributes.dart';
|
|
import 'package:flowy_editor/src/document/node.dart';
|
|
import 'package:flowy_editor/src/document/node.dart';
|
|
import 'package:flowy_editor/src/document/text_delta.dart';
|
|
import 'package:flowy_editor/src/document/text_delta.dart';
|
|
import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart';
|
|
import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart';
|
|
|
|
+import 'package:flowy_editor/src/extensions/color_extension.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:html/parser.dart' show parse;
|
|
import 'package:html/parser.dart' show parse;
|
|
import 'package:html/dom.dart' as html;
|
|
import 'package:html/dom.dart' as html;
|
|
|
|
|
|
-const String tagH1 = "h1";
|
|
|
|
-const String tagH2 = "h2";
|
|
|
|
-const String tagH3 = "h3";
|
|
|
|
-const String tagOrderedList = "ol";
|
|
|
|
-const String tagUnorderedList = "ul";
|
|
|
|
-const String tagList = "li";
|
|
|
|
-const String tagParagraph = "p";
|
|
|
|
-const String tagImage = "img";
|
|
|
|
-const String tagAnchor = "a";
|
|
|
|
-const String tagItalic = "i";
|
|
|
|
-const String tagBold = "b";
|
|
|
|
-const String tagUnderline = "u";
|
|
|
|
-const String tagDel = "del";
|
|
|
|
-const String tagStrong = "strong";
|
|
|
|
-const String tagSpan = "span";
|
|
|
|
-const String tagCode = "code";
|
|
|
|
-
|
|
|
|
-extension on Color {
|
|
|
|
- String toRgbaString() {
|
|
|
|
- return 'rgba($red, $green, $blue, $alpha)';
|
|
|
|
|
|
+class HTMLTag {
|
|
|
|
+ static const h1 = "h1";
|
|
|
|
+ static const h2 = "h2";
|
|
|
|
+ static const h3 = "h3";
|
|
|
|
+ static const orderedList = "ol";
|
|
|
|
+ static const unorderedList = "ul";
|
|
|
|
+ static const list = "li";
|
|
|
|
+ static const paragraph = "p";
|
|
|
|
+ static const image = "img";
|
|
|
|
+ static const anchor = "a";
|
|
|
|
+ static const italic = "i";
|
|
|
|
+ static const bold = "b";
|
|
|
|
+ static const underline = "u";
|
|
|
|
+ static const del = "del";
|
|
|
|
+ static const strong = "strong";
|
|
|
|
+ static const span = "span";
|
|
|
|
+ static const code = "code";
|
|
|
|
+ static const blockQuote = "blockquote";
|
|
|
|
+ static const div = "div";
|
|
|
|
+
|
|
|
|
+ static bool isTopLevel(String tag) {
|
|
|
|
+ return tag == h1 ||
|
|
|
|
+ tag == h2 ||
|
|
|
|
+ tag == h3 ||
|
|
|
|
+ tag == paragraph ||
|
|
|
|
+ tag == div ||
|
|
|
|
+ tag == blockQuote;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -54,15 +63,15 @@ class HTMLToNodesConverter {
|
|
final result = <Node>[];
|
|
final result = <Node>[];
|
|
for (final child in childNodes) {
|
|
for (final child in childNodes) {
|
|
if (child is html.Element) {
|
|
if (child is html.Element) {
|
|
- if (child.localName == tagAnchor ||
|
|
|
|
- child.localName == tagSpan ||
|
|
|
|
- child.localName == tagCode ||
|
|
|
|
- child.localName == tagStrong ||
|
|
|
|
- child.localName == tagUnderline ||
|
|
|
|
- child.localName == tagItalic ||
|
|
|
|
- child.localName == tagDel) {
|
|
|
|
|
|
+ if (child.localName == HTMLTag.anchor ||
|
|
|
|
+ child.localName == HTMLTag.span ||
|
|
|
|
+ child.localName == HTMLTag.code ||
|
|
|
|
+ child.localName == HTMLTag.strong ||
|
|
|
|
+ child.localName == HTMLTag.underline ||
|
|
|
|
+ child.localName == HTMLTag.italic ||
|
|
|
|
+ child.localName == HTMLTag.del) {
|
|
_handleRichTextElement(delta, child);
|
|
_handleRichTextElement(delta, child);
|
|
- } else if (child.localName == tagBold) {
|
|
|
|
|
|
+ } else if (child.localName == HTMLTag.bold) {
|
|
// Google docs wraps the the content inside the `<b></b>` tag.
|
|
// Google docs wraps the the content inside the `<b></b>` tag.
|
|
// It's strange
|
|
// It's strange
|
|
if (!_inParagraph) {
|
|
if (!_inParagraph) {
|
|
@@ -70,6 +79,8 @@ class HTMLToNodesConverter {
|
|
} else {
|
|
} else {
|
|
result.add(_handleRichText(child));
|
|
result.add(_handleRichText(child));
|
|
}
|
|
}
|
|
|
|
+ } else if (child.localName == HTMLTag.blockQuote) {
|
|
|
|
+ result.addAll(_handleBlockQuote(child));
|
|
} else {
|
|
} else {
|
|
result.addAll(_handleElement(child));
|
|
result.addAll(_handleElement(child));
|
|
}
|
|
}
|
|
@@ -83,6 +94,18 @@ class HTMLToNodesConverter {
|
|
return result;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ List<Node> _handleBlockQuote(html.Element element) {
|
|
|
|
+ final result = <Node>[];
|
|
|
|
+
|
|
|
|
+ for (final child in element.nodes.toList()) {
|
|
|
|
+ if (child is html.Element) {
|
|
|
|
+ result.addAll(_handleElement(child, {"subtype": StyleKey.quote}));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+
|
|
List<Node> _handleBTag(html.Element element) {
|
|
List<Node> _handleBTag(html.Element element) {
|
|
final childNodes = element.nodes;
|
|
final childNodes = element.nodes;
|
|
return _handleContainer(childNodes);
|
|
return _handleContainer(childNodes);
|
|
@@ -90,19 +113,19 @@ class HTMLToNodesConverter {
|
|
|
|
|
|
List<Node> _handleElement(html.Element element,
|
|
List<Node> _handleElement(html.Element element,
|
|
[Map<String, dynamic>? attributes]) {
|
|
[Map<String, dynamic>? attributes]) {
|
|
- if (element.localName == tagH1) {
|
|
|
|
- return [_handleHeadingElement(element, tagH1)];
|
|
|
|
- } else if (element.localName == tagH2) {
|
|
|
|
- return [_handleHeadingElement(element, tagH2)];
|
|
|
|
- } else if (element.localName == tagH3) {
|
|
|
|
- return [_handleHeadingElement(element, tagH3)];
|
|
|
|
- } else if (element.localName == tagUnorderedList) {
|
|
|
|
|
|
+ if (element.localName == HTMLTag.h1) {
|
|
|
|
+ return [_handleHeadingElement(element, HTMLTag.h1)];
|
|
|
|
+ } else if (element.localName == HTMLTag.h2) {
|
|
|
|
+ return [_handleHeadingElement(element, HTMLTag.h2)];
|
|
|
|
+ } else if (element.localName == HTMLTag.h3) {
|
|
|
|
+ return [_handleHeadingElement(element, HTMLTag.h3)];
|
|
|
|
+ } else if (element.localName == HTMLTag.unorderedList) {
|
|
return _handleUnorderedList(element);
|
|
return _handleUnorderedList(element);
|
|
- } else if (element.localName == tagOrderedList) {
|
|
|
|
|
|
+ } else if (element.localName == HTMLTag.orderedList) {
|
|
return _handleOrderedList(element);
|
|
return _handleOrderedList(element);
|
|
- } else if (element.localName == tagList) {
|
|
|
|
|
|
+ } else if (element.localName == HTMLTag.list) {
|
|
return _handleListElement(element);
|
|
return _handleListElement(element);
|
|
- } else if (element.localName == tagParagraph) {
|
|
|
|
|
|
+ } else if (element.localName == HTMLTag.paragraph) {
|
|
return [_handleParagraph(element, attributes)];
|
|
return [_handleParagraph(element, attributes)];
|
|
} else {
|
|
} else {
|
|
final delta = Delta();
|
|
final delta = Delta();
|
|
@@ -164,7 +187,9 @@ class HTMLToNodesConverter {
|
|
}
|
|
}
|
|
|
|
|
|
final backgroundColorStr = cssMap["background-color"];
|
|
final backgroundColorStr = cssMap["background-color"];
|
|
- final backgroundColor = _tryParseCssColorString(backgroundColorStr);
|
|
|
|
|
|
+ final backgroundColor = backgroundColorStr == null
|
|
|
|
+ ? null
|
|
|
|
+ : ColorExtension.tryFromRgbaString(backgroundColorStr);
|
|
if (backgroundColor != null) {
|
|
if (backgroundColor != null) {
|
|
attrs[StyleKey.backgroundColor] =
|
|
attrs[StyleKey.backgroundColor] =
|
|
'0x${backgroundColor.value.toRadixString(16)}';
|
|
'0x${backgroundColor.value.toRadixString(16)}';
|
|
@@ -188,56 +213,25 @@ class HTMLToNodesConverter {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- /// Try to parse the `rgba(red, greed, blue, alpha)`
|
|
|
|
- /// from the string.
|
|
|
|
- Color? _tryParseCssColorString(String? colorString) {
|
|
|
|
- if (colorString == null) {
|
|
|
|
- return null;
|
|
|
|
- }
|
|
|
|
- final reg = RegExp(r'rgba\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)');
|
|
|
|
- final match = reg.firstMatch(colorString);
|
|
|
|
- if (match == null) {
|
|
|
|
- return null;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (match.groupCount < 4) {
|
|
|
|
- return null;
|
|
|
|
- }
|
|
|
|
- final redStr = match.group(1);
|
|
|
|
- final greenStr = match.group(2);
|
|
|
|
- final blueStr = match.group(3);
|
|
|
|
- final alphaStr = match.group(4);
|
|
|
|
-
|
|
|
|
- final red = redStr != null ? int.tryParse(redStr) : null;
|
|
|
|
- final green = greenStr != null ? int.tryParse(greenStr) : null;
|
|
|
|
- final blue = blueStr != null ? int.tryParse(blueStr) : null;
|
|
|
|
- final alpha = alphaStr != null ? int.tryParse(alphaStr) : null;
|
|
|
|
-
|
|
|
|
- if (red == null || green == null || blue == null || alpha == null) {
|
|
|
|
- return null;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return Color.fromARGB(alpha, red, green, blue);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
_handleRichTextElement(Delta delta, html.Element element) {
|
|
_handleRichTextElement(Delta delta, html.Element element) {
|
|
- if (element.localName == tagSpan) {
|
|
|
|
|
|
+ if (element.localName == HTMLTag.span) {
|
|
delta.insert(element.text,
|
|
delta.insert(element.text,
|
|
_getDeltaAttributesFromHtmlAttributes(element.attributes));
|
|
_getDeltaAttributesFromHtmlAttributes(element.attributes));
|
|
- } else if (element.localName == tagAnchor) {
|
|
|
|
|
|
+ } else if (element.localName == HTMLTag.anchor) {
|
|
final hyperLink = element.attributes["href"];
|
|
final hyperLink = element.attributes["href"];
|
|
Map<String, dynamic>? attributes;
|
|
Map<String, dynamic>? attributes;
|
|
if (hyperLink != null) {
|
|
if (hyperLink != null) {
|
|
attributes = {"href": hyperLink};
|
|
attributes = {"href": hyperLink};
|
|
}
|
|
}
|
|
delta.insert(element.text, attributes);
|
|
delta.insert(element.text, attributes);
|
|
- } else if (element.localName == tagStrong || element.localName == tagBold) {
|
|
|
|
|
|
+ } else if (element.localName == HTMLTag.strong ||
|
|
|
|
+ element.localName == HTMLTag.bold) {
|
|
delta.insert(element.text, {StyleKey.bold: true});
|
|
delta.insert(element.text, {StyleKey.bold: true});
|
|
- } else if (element.localName == tagUnderline) {
|
|
|
|
|
|
+ } else if (element.localName == HTMLTag.underline) {
|
|
delta.insert(element.text, {StyleKey.underline: true});
|
|
delta.insert(element.text, {StyleKey.underline: true});
|
|
- } else if (element.localName == tagItalic) {
|
|
|
|
|
|
+ } else if (element.localName == HTMLTag.italic) {
|
|
delta.insert(element.text, {StyleKey.italic: true});
|
|
delta.insert(element.text, {StyleKey.italic: true});
|
|
- } else if (element.localName == tagDel) {
|
|
|
|
|
|
+ } else if (element.localName == HTMLTag.del) {
|
|
delta.insert(element.text, {StyleKey.strikethrough: true});
|
|
delta.insert(element.text, {StyleKey.strikethrough: true});
|
|
} else {
|
|
} else {
|
|
delta.insert(element.text);
|
|
delta.insert(element.text);
|
|
@@ -250,7 +244,7 @@ class HTMLToNodesConverter {
|
|
/// A container contains a <img /> will be regarded as a image block
|
|
/// A container contains a <img /> will be regarded as a image block
|
|
Node _handleRichText(html.Element element,
|
|
Node _handleRichText(html.Element element,
|
|
[Map<String, dynamic>? attributes]) {
|
|
[Map<String, dynamic>? attributes]) {
|
|
- final image = element.querySelector(tagImage);
|
|
|
|
|
|
+ final image = element.querySelector(HTMLTag.image);
|
|
if (image != null) {
|
|
if (image != null) {
|
|
final imageNode = _handleImage(image);
|
|
final imageNode = _handleImage(image);
|
|
return imageNode;
|
|
return imageNode;
|
|
@@ -404,10 +398,10 @@ class NodesToHTMLConverter {
|
|
}
|
|
}
|
|
|
|
|
|
_addElement(TextNode textNode, html.Element element) {
|
|
_addElement(TextNode textNode, html.Element element) {
|
|
- if (element.localName == tagList) {
|
|
|
|
|
|
+ if (element.localName == HTMLTag.list) {
|
|
final isNumbered = textNode.attributes["subtype"] == StyleKey.numberList;
|
|
final isNumbered = textNode.attributes["subtype"] == StyleKey.numberList;
|
|
- _stashListContainer ??=
|
|
|
|
- html.Element.tag(isNumbered ? tagOrderedList : tagUnorderedList);
|
|
|
|
|
|
+ _stashListContainer ??= html.Element.tag(
|
|
|
|
+ isNumbered ? HTMLTag.orderedList : HTMLTag.unorderedList);
|
|
_stashListContainer?.append(element);
|
|
_stashListContainer?.append(element);
|
|
} else {
|
|
} else {
|
|
if (_stashListContainer != null) {
|
|
if (_stashListContainer != null) {
|
|
@@ -427,8 +421,10 @@ class NodesToHTMLConverter {
|
|
|
|
|
|
html.Element _textNodeToHtml(TextNode textNode, {int? end}) {
|
|
html.Element _textNodeToHtml(TextNode textNode, {int? end}) {
|
|
String? subType = textNode.attributes["subtype"];
|
|
String? subType = textNode.attributes["subtype"];
|
|
|
|
+ String? heading = textNode.attributes["heading"];
|
|
return _deltaToHtml(textNode.delta,
|
|
return _deltaToHtml(textNode.delta,
|
|
subType: subType,
|
|
subType: subType,
|
|
|
|
+ heading: heading,
|
|
end: end,
|
|
end: end,
|
|
checked: textNode.attributes["checkbox"] == true);
|
|
checked: textNode.attributes["checkbox"] == true);
|
|
}
|
|
}
|
|
@@ -501,22 +497,32 @@ class NodesToHTMLConverter {
|
|
/// <span style="...">Text</span>
|
|
/// <span style="...">Text</span>
|
|
/// ```
|
|
/// ```
|
|
html.Element _deltaToHtml(Delta delta,
|
|
html.Element _deltaToHtml(Delta delta,
|
|
- {String? subType, int? end, bool? checked}) {
|
|
|
|
|
|
+ {String? subType, String? heading, int? end, bool? checked}) {
|
|
if (end != null) {
|
|
if (end != null) {
|
|
delta = delta.slice(0, end);
|
|
delta = delta.slice(0, end);
|
|
}
|
|
}
|
|
|
|
|
|
final childNodes = <html.Node>[];
|
|
final childNodes = <html.Node>[];
|
|
- String tagName = tagParagraph;
|
|
|
|
|
|
+ String tagName = HTMLTag.paragraph;
|
|
|
|
|
|
if (subType == StyleKey.bulletedList || subType == StyleKey.numberList) {
|
|
if (subType == StyleKey.bulletedList || subType == StyleKey.numberList) {
|
|
- tagName = tagList;
|
|
|
|
|
|
+ tagName = HTMLTag.list;
|
|
} else if (subType == StyleKey.checkbox) {
|
|
} else if (subType == StyleKey.checkbox) {
|
|
final node = html.Element.html('<input type="checkbox" />');
|
|
final node = html.Element.html('<input type="checkbox" />');
|
|
if (checked != null && checked) {
|
|
if (checked != null && checked) {
|
|
node.attributes["checked"] = "true";
|
|
node.attributes["checked"] = "true";
|
|
}
|
|
}
|
|
childNodes.add(node);
|
|
childNodes.add(node);
|
|
|
|
+ } else if (subType == StyleKey.heading) {
|
|
|
|
+ if (heading == StyleKey.h1) {
|
|
|
|
+ tagName = HTMLTag.h1;
|
|
|
|
+ } else if (heading == StyleKey.h2) {
|
|
|
|
+ tagName = HTMLTag.h2;
|
|
|
|
+ } else if (heading == StyleKey.h3) {
|
|
|
|
+ tagName = HTMLTag.h3;
|
|
|
|
+ }
|
|
|
|
+ } else if (subType == StyleKey.quote) {
|
|
|
|
+ tagName = HTMLTag.blockQuote;
|
|
}
|
|
}
|
|
|
|
|
|
for (final op in delta) {
|
|
for (final op in delta) {
|
|
@@ -524,26 +530,26 @@ class NodesToHTMLConverter {
|
|
final attributes = op.attributes;
|
|
final attributes = op.attributes;
|
|
if (attributes != null) {
|
|
if (attributes != null) {
|
|
if (attributes.length == 1 && attributes[StyleKey.bold] == true) {
|
|
if (attributes.length == 1 && attributes[StyleKey.bold] == true) {
|
|
- final strong = html.Element.tag(tagStrong);
|
|
|
|
|
|
+ final strong = html.Element.tag(HTMLTag.strong);
|
|
strong.append(html.Text(op.content));
|
|
strong.append(html.Text(op.content));
|
|
childNodes.add(strong);
|
|
childNodes.add(strong);
|
|
} else if (attributes.length == 1 &&
|
|
} else if (attributes.length == 1 &&
|
|
attributes[StyleKey.underline] == true) {
|
|
attributes[StyleKey.underline] == true) {
|
|
- final strong = html.Element.tag(tagUnderline);
|
|
|
|
|
|
+ final strong = html.Element.tag(HTMLTag.underline);
|
|
strong.append(html.Text(op.content));
|
|
strong.append(html.Text(op.content));
|
|
childNodes.add(strong);
|
|
childNodes.add(strong);
|
|
} else if (attributes.length == 1 &&
|
|
} else if (attributes.length == 1 &&
|
|
attributes[StyleKey.italic] == true) {
|
|
attributes[StyleKey.italic] == true) {
|
|
- final strong = html.Element.tag(tagItalic);
|
|
|
|
|
|
+ final strong = html.Element.tag(HTMLTag.italic);
|
|
strong.append(html.Text(op.content));
|
|
strong.append(html.Text(op.content));
|
|
childNodes.add(strong);
|
|
childNodes.add(strong);
|
|
} else if (attributes.length == 1 &&
|
|
} else if (attributes.length == 1 &&
|
|
attributes[StyleKey.strikethrough] == true) {
|
|
attributes[StyleKey.strikethrough] == true) {
|
|
- final strong = html.Element.tag(tagDel);
|
|
|
|
|
|
+ final strong = html.Element.tag(HTMLTag.del);
|
|
strong.append(html.Text(op.content));
|
|
strong.append(html.Text(op.content));
|
|
childNodes.add(strong);
|
|
childNodes.add(strong);
|
|
} else {
|
|
} else {
|
|
- final span = html.Element.tag(tagSpan);
|
|
|
|
|
|
+ final span = html.Element.tag(HTMLTag.span);
|
|
final cssString = _attributesToCssStyle(attributes);
|
|
final cssString = _attributesToCssStyle(attributes);
|
|
if (cssString.isNotEmpty) {
|
|
if (cssString.isNotEmpty) {
|
|
span.attributes["style"] = cssString;
|
|
span.attributes["style"] = cssString;
|
|
@@ -557,12 +563,20 @@ class NodesToHTMLConverter {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- if (tagName != tagParagraph) {
|
|
|
|
- final p = html.Element.tag(tagParagraph);
|
|
|
|
|
|
+ if (tagName == HTMLTag.blockQuote) {
|
|
|
|
+ final p = html.Element.tag(HTMLTag.paragraph);
|
|
|
|
+ for (final node in childNodes) {
|
|
|
|
+ p.append(node);
|
|
|
|
+ }
|
|
|
|
+ final blockQuote = html.Element.tag(tagName);
|
|
|
|
+ blockQuote.append(p);
|
|
|
|
+ return blockQuote;
|
|
|
|
+ } else if (!HTMLTag.isTopLevel(tagName)) {
|
|
|
|
+ final p = html.Element.tag(HTMLTag.paragraph);
|
|
for (final node in childNodes) {
|
|
for (final node in childNodes) {
|
|
p.append(node);
|
|
p.append(node);
|
|
}
|
|
}
|
|
- final result = html.Element.tag(tagList);
|
|
|
|
|
|
+ final result = html.Element.tag(HTMLTag.list);
|
|
result.append(p);
|
|
result.append(p);
|
|
return result;
|
|
return result;
|
|
} else {
|
|
} else {
|