diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index 5a1a7912da..708e47cb85 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -4,6 +4,7 @@ import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/text_delta.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; @@ -205,20 +206,58 @@ class HTMLConverter { } } -String deltaToHtml(Delta delta) { - var result = "

"; +html.Element deltaToHtml(Delta delta, [String? subType]) { + final childNodes = []; + String tagName = tagParagraph; + + if (subType == "bulleted-list") { + tagName = tagList; + } for (final op in delta.operations) { if (op is TextInsert) { final attributes = op.attributes; if (attributes != null && attributes["bold"] == true) { - result += '${op.content}'; + final strong = html.Element.tag("strong"); + strong.append(html.Text(op.content)); + childNodes.add(strong); } else { - result += op.content; + childNodes.add(html.Text(op.content)); } } } - result += "

"; - return result; + if (tagName != tagParagraph) { + final p = html.Element.tag(tagParagraph); + for (final node in childNodes) { + p.append(node); + } + 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); + } + return p; + } +} + +String stringify(html.Node node) { + if (node is html.Element) { + String result = '<${node.localName}>'; + + for (final node in node.nodes) { + result += stringify(node); + } + + return result += ''; + } + + if (node is html.Text) { + return node.text; + } + + return ""; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index 8cab09b293..d3eeb073b9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,3 +1,4 @@ +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'; @@ -6,6 +7,50 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; +class _HTMLNormalizer { + final List nodes; + html.Element? _pendingList; + + _HTMLNormalizer(this.nodes); + + List normalize() { + final result = []; + + 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 result) { + if (_pendingList == null) { + return; + } + result.add(_pendingList!); + _pendingList = null; + } +} + _handleCopy(EditorState editorState) async { final selection = editorState.cursorSelection; if (selection == null || selection.isCollapsed) { @@ -18,7 +63,7 @@ _handleCopy(EditorState editorState) async { final delta = textNode.delta.slice(selection.start.offset, selection.end.offset); - final htmlString = deltaToHtml(delta); + final htmlString = stringify(deltaToHtml(delta)); debugPrint('copy html: $htmlString'); RichClipboard.setData(RichClipboardData(html: htmlString)); } else { @@ -31,27 +76,30 @@ _handleCopy(EditorState editorState) async { final endNode = editorState.document.nodeAtPath(selection.end.path)!; final traverser = NodeIterator(editorState.document, beginNode, endNode); - var copyString = ""; + final nodes = []; while (traverser.moveNext()) { final node = traverser.current; if (node.type == "text") { final textNode = node as TextNode; + String? subType = textNode.attributes["subtype"]; if (node == beginNode) { - final htmlString = - deltaToHtml(textNode.delta.slice(selection.start.offset)); - copyString += htmlString; + final htmlElement = + deltaToHtml(textNode.delta.slice(selection.start.offset), subType); + nodes.add(htmlElement); } else if (node == endNode) { - final htmlString = - deltaToHtml(textNode.delta.slice(0, selection.end.offset)); - copyString += htmlString; + final htmlElement = + deltaToHtml(textNode.delta.slice(0, selection.end.offset), subType); + nodes.add(htmlElement); } else { - final htmlString = deltaToHtml(textNode.delta); - copyString += htmlString; + final htmlElement = deltaToHtml(textNode.delta, subType); + nodes.add(htmlElement); } } // TODO: handle image and other blocks - } + + final copyString = _HTMLNormalizer(nodes).normalize().fold( + "", ((previousValue, element) => previousValue + stringify(element))); debugPrint('copy html: $copyString'); RichClipboard.setData(RichClipboardData(html: copyString)); }