Explorar o código

feat: paste ordered list

Vincent Chan %!s(int64=2) %!d(string=hai) anos
pai
achega
4140994fed

+ 181 - 94
frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart

@@ -4,14 +4,13 @@ import 'package:flowy_editor/document/attributes.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/text_delta.dart';
 import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
 import 'package:html/parser.dart' show parse;
 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";
@@ -22,23 +21,21 @@ const String tagStrong = "strong";
 const String tagSpan = "span";
 const String tagCode = "code";
 
-class HTMLConverter {
+/// Converting the HTML to nodes
+class HTMLToNodesConverter {
   final html.Document _document;
   bool _inParagraph = false;
 
-  HTMLConverter(String htmlString) : _document = parse(htmlString);
+  HTMLToNodesConverter(String htmlString) : _document = parse(htmlString);
 
   List<Node> toNodes() {
-    final result = <Node>[];
-
     final childNodes = _document.body?.nodes.toList() ?? <html.Node>[];
-    _handleContainer(result, childNodes);
-
-    return result;
+    return _handleContainer(childNodes);
   }
 
-  _handleContainer(List<Node> nodes, List<html.Node> childNodes) {
+  List<Node> _handleContainer(List<html.Node> childNodes) {
     final delta = Delta();
+    final result = <Node>[];
     for (final child in childNodes) {
       if (child is html.Element) {
         if (child.localName == tagAnchor ||
@@ -50,55 +47,60 @@ class HTMLConverter {
           // Google docs wraps the the content inside the <b></b> tag.
           // It's strange
           if (!_inParagraph) {
-            _handleBTag(nodes, child);
+            result.addAll(_handleBTag(child));
           } else {
-            _handleRichText(nodes, child);
+            result.add(_handleRichText(child));
           }
         } else {
-          _handleElement(nodes, child);
+          result.addAll(_handleElement(child));
         }
       } else {
         delta.insert(child.text ?? "");
       }
     }
     if (delta.operations.isNotEmpty) {
-      nodes.add(TextNode(type: "text", delta: delta));
+      result.add(TextNode(type: "text", delta: delta));
     }
+    return result;
   }
 
-  _handleBTag(List<Node> nodes, html.Element element) {
+  List<Node> _handleBTag(html.Element element) {
     final childNodes = element.nodes;
-    _handleContainer(nodes, childNodes);
+    return _handleContainer(childNodes);
   }
 
-  _handleElement(List<Node> nodes, html.Element element,
+  List<Node> _handleElement(html.Element element,
       [Map<String, dynamic>? attributes]) {
     if (element.localName == tagH1) {
-      _handleHeadingElement(nodes, element, tagH1);
+      return [_handleHeadingElement(element, tagH1)];
     } else if (element.localName == tagH2) {
-      _handleHeadingElement(nodes, element, tagH2);
+      return [_handleHeadingElement(element, tagH2)];
     } else if (element.localName == tagH3) {
-      _handleHeadingElement(nodes, element, tagH3);
+      return [_handleHeadingElement(element, tagH3)];
     } else if (element.localName == tagUnorderedList) {
-      _handleUnorderedList(nodes, element);
+      return _handleUnorderedList(element);
+    } else if (element.localName == tagOrderedList) {
+      return _handleOrderedList(element);
     } else if (element.localName == tagList) {
-      _handleListElement(nodes, element);
+      return _handleListElement(element);
     } else if (element.localName == tagParagraph) {
-      _handleParagraph(nodes, element, attributes);
+      return [_handleParagraph(element, attributes)];
     } else {
       final delta = Delta();
       delta.insert(element.text);
       if (delta.operations.isNotEmpty) {
-        nodes.add(TextNode(type: "text", delta: delta));
+        return [TextNode(type: "text", delta: delta)];
       }
     }
+    return [];
   }
 
-  _handleParagraph(List<Node> nodes, html.Element element,
+  Node _handleParagraph(html.Element element,
       [Map<String, dynamic>? attributes]) {
     _inParagraph = true;
-    _handleRichText(nodes, element, attributes);
+    final node = _handleRichText(element, attributes);
     _inParagraph = false;
+    return node;
   }
 
   Attributes? _getDeltaAttributesFromHtmlAttributes(
@@ -142,12 +144,12 @@ class HTMLConverter {
     }
   }
 
-  _handleRichText(List<Node> nodes, html.Element element,
+  Node _handleRichText(html.Element element,
       [Map<String, dynamic>? attributes]) {
     final image = element.querySelector(tagImage);
     if (image != null) {
-      _handleImage(nodes, image);
-      return;
+      final imageNode = _handleImage(image);
+      return imageNode;
     }
     final testInput = element.querySelector("input");
     bool checked = false;
@@ -168,112 +170,197 @@ class HTMLConverter {
       }
     }
 
-    if (delta.operations.isNotEmpty) {
-      final textNode = TextNode(type: "text", delta: delta);
-      if (isCheckbox) {
-        textNode.attributes["subtype"] = StyleKey.checkbox;
-        textNode.attributes["checkbox"] = checked;
-      }
-      nodes.add(textNode);
+    final textNode = TextNode(type: "text", delta: delta);
+    if (isCheckbox) {
+      textNode.attributes["subtype"] = StyleKey.checkbox;
+      textNode.attributes["checkbox"] = checked;
     }
+    return textNode;
   }
 
-  _handleImage(List<Node> nodes, html.Element element) {
+  Node _handleImage(html.Element element) {
     final src = element.attributes["src"];
     final attributes = <String, dynamic>{};
     if (src != null) {
       attributes["image_src"] = src;
     }
-    debugPrint("insert image: $src");
-    nodes.add(
-        Node(type: "image", attributes: attributes, children: LinkedList()));
+    return Node(type: "image", attributes: attributes, children: LinkedList());
+  }
+
+  List<Node> _handleUnorderedList(html.Element element) {
+    final result = <Node>[];
+    element.children.forEach((child) {
+      result.addAll(
+          _handleListElement(child, {"subtype": StyleKey.bulletedList}));
+    });
+    return result;
   }
 
-  _handleUnorderedList(List<Node> nodes, html.Element element) {
+  List<Node> _handleOrderedList(html.Element element) {
+    final result = <Node>[];
     element.children.forEach((child) {
-      _handleListElement(nodes, child);
+      result
+          .addAll(_handleListElement(child, {"subtype": StyleKey.numberList}));
     });
+    return result;
   }
 
-  _handleHeadingElement(
-    List<Node> nodes,
+  Node _handleHeadingElement(
     html.Element element,
     String headingStyle,
   ) {
     final delta = Delta();
     delta.insert(element.text);
-    if (delta.operations.isNotEmpty) {
-      nodes.add(TextNode(
-          type: "text",
-          attributes: {"subtype": "heading", "heading": headingStyle},
-          delta: delta));
-    }
+    return TextNode(
+        type: "text",
+        attributes: {"subtype": "heading", "heading": headingStyle},
+        delta: delta);
   }
 
-  _handleListElement(List<Node> nodes, html.Element element) {
+  List<Node> _handleListElement(html.Element element,
+      [Map<String, dynamic>? attributes]) {
+    final result = <Node>[];
     final childNodes = element.nodes.toList();
     for (final child in childNodes) {
       if (child is html.Element) {
-        _handleElement(nodes, child, {"subtype": "bulleted-list"});
+        result.addAll(_handleElement(child, attributes));
       }
     }
+    return result;
   }
 }
 
-html.Element textNodeToHtml(TextNode textNode, {int? end, bool? checked}) {
-  String? subType = textNode.attributes["subtype"];
-  return deltaToHtml(textNode.delta,
-      subType: subType,
-      end: end,
-      checked: textNode.attributes["checkbox"] == true);
-}
+class _HTMLNormalizer {
+  final List<html.Node> nodes;
+  html.Element? _pendingList;
 
-html.Element deltaToHtml(Delta delta,
-    {String? subType, int? end, bool? checked}) {
-  if (end != null) {
-    delta = delta.slice(0, end);
-  }
+  _HTMLNormalizer(this.nodes);
+
+  List<html.Node> normalize() {
+    final result = <html.Node>[];
+
+    for (final item in nodes) {
+      if (item is html.Text) {
+        result.add(item);
+        continue;
+      }
+
+      if (item is html.Element) {
+        if (item.localName == "li") {
+          if (_pendingList != null) {
+            _pendingList!.append(item);
+          } else {
+            final ulItem = html.Element.tag("ul");
+            ulItem.append(item);
+
+            _pendingList = ulItem;
+          }
+        } else {
+          _pushList(result);
+          result.add(item);
+        }
+      }
+    }
 
-  final childNodes = <html.Node>[];
-  String tagName = tagParagraph;
+    return result;
+  }
 
-  if (subType == StyleKey.bulletedList) {
-    tagName = tagList;
-  } else if (subType == StyleKey.checkbox) {
-    final node = html.Element.html('<input type="checkbox" />');
-    if (checked != null && checked) {
-      node.attributes["checked"] = "true";
+  _pushList(List<html.Node> result) {
+    if (_pendingList == null) {
+      return;
     }
-    childNodes.add(node);
+    result.add(_pendingList!);
+    _pendingList = null;
   }
+}
 
-  for (final op in delta.operations) {
-    if (op is TextInsert) {
-      final attributes = op.attributes;
-      if (attributes != null && attributes[StyleKey.bold] == true) {
-        final strong = html.Element.tag("strong");
-        strong.append(html.Text(op.content));
-        childNodes.add(strong);
-      } else {
-        childNodes.add(html.Text(op.content));
+class NodesToHTMLConverter {
+  final List<Node> nodes;
+  final int? startOffset;
+  final int? endOffset;
+
+  NodesToHTMLConverter({required this.nodes, this.startOffset, this.endOffset});
+
+  List<html.Node> toHTMLNodes() {
+    final result = <html.Node>[];
+    for (final node in nodes) {
+      if (node.type == "text") {
+        final textNode = node as TextNode;
+        if (node == nodes.first) {
+          result.add(_textNodeToHtml(textNode));
+        } else if (node == nodes.last) {
+          result.add(_textNodeToHtml(textNode, end: endOffset));
+        } else {
+          result.add(_textNodeToHtml(textNode));
+        }
       }
+      // TODO: handle image and other blocks
     }
+    return result;
+  }
+
+  String toHTMLString() {
+    final elements = toHTMLNodes();
+    final copyString = _HTMLNormalizer(elements).normalize().fold<String>(
+        "", ((previousValue, element) => previousValue + stringify(element)));
+    return copyString;
   }
 
-  if (tagName != tagParagraph) {
-    final p = html.Element.tag(tagParagraph);
-    for (final node in childNodes) {
-      p.append(node);
+  html.Element _textNodeToHtml(TextNode textNode, {int? end}) {
+    String? subType = textNode.attributes["subtype"];
+    return _deltaToHtml(textNode.delta,
+        subType: subType,
+        end: end,
+        checked: textNode.attributes["checkbox"] == true);
+  }
+
+  html.Element _deltaToHtml(Delta delta,
+      {String? subType, int? end, bool? checked}) {
+    if (end != null) {
+      delta = delta.slice(0, end);
     }
-    final result = html.Element.tag("li");
-    result.append(p);
-    return result;
-  } else {
-    final p = html.Element.tag(tagName);
-    for (final node in childNodes) {
-      p.append(node);
+
+    final childNodes = <html.Node>[];
+    String tagName = tagParagraph;
+
+    if (subType == StyleKey.bulletedList || subType == StyleKey.numberList) {
+      tagName = tagList;
+    } else if (subType == StyleKey.checkbox) {
+      final node = html.Element.html('<input type="checkbox" />');
+      if (checked != null && checked) {
+        node.attributes["checked"] = "true";
+      }
+      childNodes.add(node);
+    }
+
+    for (final op in delta.operations) {
+      if (op is TextInsert) {
+        final attributes = op.attributes;
+        if (attributes != null && attributes[StyleKey.bold] == true) {
+          final strong = html.Element.tag("strong");
+          strong.append(html.Text(op.content));
+          childNodes.add(strong);
+        } else {
+          childNodes.add(html.Text(op.content));
+        }
+      }
+    }
+
+    if (tagName != tagParagraph) {
+      final p = html.Element.tag(tagParagraph);
+      for (final node in childNodes) {
+        p.append(node);
+      }
+      final result = html.Element.tag(tagList);
+      result.append(p);
+      return result;
+    } else {
+      final p = html.Element.tag(tagName);
+      for (final node in childNodes) {
+        p.append(node);
+      }
+      return p;
     }
-    return p;
   }
 }
 

+ 4 - 66
frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart

@@ -1,4 +1,3 @@
-import 'package:html/dom.dart' as html;
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flowy_editor/service/keyboard_service.dart';
 import 'package:flowy_editor/infra/html_converter.dart';
@@ -7,50 +6,6 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:rich_clipboard/rich_clipboard.dart';
 
-class _HTMLNormalizer {
-  final List<html.Node> nodes;
-  html.Element? _pendingList;
-
-  _HTMLNormalizer(this.nodes);
-
-  List<html.Node> normalize() {
-    final result = <html.Node>[];
-
-    for (final item in nodes) {
-      if (item is Text) {
-        result.add(item);
-        continue;
-      }
-
-      if (item is html.Element) {
-        if (item.localName == "li") {
-          if (_pendingList != null) {
-            _pendingList!.append(item);
-          } else {
-            final ulItem = html.Element.tag("ul");
-            ulItem.append(item);
-
-            _pendingList = ulItem;
-          }
-        } else {
-          _pushList(result);
-          result.add(item);
-        }
-      }
-    }
-
-    return result;
-  }
-
-  _pushList(List<html.Node> result) {
-    if (_pendingList == null) {
-      return;
-    }
-    result.add(_pendingList!);
-    _pendingList = null;
-  }
-}
-
 _handleCopy(EditorState editorState) async {
   final selection = editorState.cursorSelection;
   if (selection == null || selection.isCollapsed) {
@@ -60,7 +15,7 @@ _handleCopy(EditorState editorState) async {
     final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!;
     if (nodeAtPath.type == "text") {
       final textNode = nodeAtPath as TextNode;
-      final htmlString = stringify(textNodeToHtml(textNode));
+      final htmlString = NodesToHTMLConverter(nodes: [textNode]).toHTMLString();
       debugPrint('copy html: $htmlString');
       RichClipboard.setData(RichClipboardData(html: htmlString));
     } else {
@@ -71,26 +26,10 @@ _handleCopy(EditorState editorState) async {
 
   final beginNode = editorState.document.nodeAtPath(selection.start.path)!;
   final endNode = editorState.document.nodeAtPath(selection.end.path)!;
-  final traverser = NodeIterator(editorState.document, beginNode, endNode);
 
-  final nodes = <html.Node>[];
-  while (traverser.moveNext()) {
-    final node = traverser.current;
-    if (node.type == "text") {
-      final textNode = node as TextNode;
-      if (node == beginNode) {
-        nodes.add(textNodeToHtml(textNode));
-      } else if (node == endNode) {
-        nodes.add(textNodeToHtml(textNode, end: selection.end.offset));
-      } else {
-        nodes.add(textNodeToHtml(textNode));
-      }
-    }
-    // TODO: handle image and other blocks
-  }
+  final nodes = NodeIterator(editorState.document, beginNode, endNode).toList();
 
-  final copyString = _HTMLNormalizer(nodes).normalize().fold<String>(
-      "", ((previousValue, element) => previousValue + stringify(element)));
+  final copyString = NodesToHTMLConverter(nodes: nodes).toHTMLString();
   debugPrint('copy html: $copyString');
   RichClipboard.setData(RichClipboardData(html: copyString));
 }
@@ -107,8 +46,7 @@ _pasteHTML(EditorState editorState, String html) {
   }
 
   debugPrint('paste html: $html');
-  final converter = HTMLConverter(html);
-  final nodes = converter.toNodes();
+  final nodes = HTMLToNodesConverter(html).toNodes();
 
   if (nodes.isEmpty) {
     return;