From 69a61612b14d978edf467cce5e511fde274f3f3a Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Thu, 3 Aug 2023 21:56:54 +0200 Subject: [PATCH] fix: paste links Closes: #328 --- .../paste_command.dart | 2 +- .../decoder/document_markdown_decoder.dart | 70 ++++++++++++------- .../copy_paste_handler.dart | 29 +++++--- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/paste_command.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/paste_command.dart index 51e4f77d8..a464147c1 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/paste_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/paste_command.dart @@ -95,7 +95,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) { if (html != null && html.isNotEmpty) { pasteHTML(editorState, html); } else if (text != null && text.isNotEmpty) { - handlePastePlainText(editorState, data.text!); + handlePastePlainText(editorState, text); } }(); diff --git a/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart b/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart index 62b8205f7..3618f047d 100644 --- a/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart +++ b/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart @@ -9,6 +9,11 @@ class DocumentMarkdownDecoder extends Converter { final htmlRegex = RegExp('^(http|https)://'); final numberedlistRegex = RegExp(r'^\d+\. '); + // Reference: https://stackoverflow.com/a/6041965 + final linkRegExp = RegExp( + r"(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])", + ); + @override Document convert(String input) { final lines = input.split('\n'); @@ -62,36 +67,32 @@ class DocumentMarkdownDecoder extends Converter { if (line.startsWith('### ')) { return headingNode( level: 3, - attributes: {'delta': decoder.convert(line.substring(4)).toJson()}, + delta: decoder.convert(line.substring(4)), ); } else if (line.startsWith('## ')) { return headingNode( level: 2, - attributes: {'delta': decoder.convert(line.substring(3)).toJson()}, + delta: decoder.convert(line.substring(3)), ); } else if (line.startsWith('# ')) { return headingNode( level: 1, - attributes: {'delta': decoder.convert(line.substring(2)).toJson()}, + delta: decoder.convert(line.substring(2)), ); } else if (line.startsWith('- [ ] ')) { return todoListNode( checked: false, - attributes: {'delta': decoder.convert(line.substring(6)).toJson()}, + delta: decoder.convert(line.substring(6)), ); } else if (line.startsWith('- [x] ')) { return todoListNode( checked: true, - attributes: {'delta': decoder.convert(line.substring(6)).toJson()}, + delta: decoder.convert(line.substring(6)), ); } else if (line.startsWith('> ')) { - return quoteNode( - attributes: {'delta': decoder.convert(line.substring(2)).toJson()}, - ); + return quoteNode(delta: decoder.convert(line.substring(2))); } else if (line.startsWith('- ') || line.startsWith('* ')) { - return bulletedListNode( - attributes: {'delta': decoder.convert(line.substring(2)).toJson()}, - ); + return bulletedListNode(delta: decoder.convert(line.substring(2))); } else if (line.isNotEmpty && RegExp('^-*').stringMatch(line) == line) { return Node(type: 'divider'); } else if (line.startsWith('```') && line.endsWith('```')) { @@ -112,22 +113,18 @@ class DocumentMarkdownDecoder extends Converter { } } else if (numberedlistRegex.hasMatch(line)) { return numberedListNode( - attributes: { - 'delta': - decoder.convert(line.substring(line.indexOf('.') + 1)).toJson() - }, + delta: decoder.convert(line.substring(line.indexOf('.') + 1)), ); + } else if (linkRegExp.hasMatch(line)) { + final delta = deltaFromLineWithLinks(line); + return paragraphNode(delta: delta); } if (line.isNotEmpty) { - return paragraphNode( - attributes: {'delta': decoder.convert(line).toJson()}, - ); + return paragraphNode(delta: decoder.convert(line)); } - return paragraphNode( - attributes: {'delta': Delta().toJson()}, - ); + return paragraphNode(delta: Delta()); } String? extractImagePath(String text) { @@ -140,6 +137,29 @@ class DocumentMarkdownDecoder extends Converter { return match?.group(1); } + Delta deltaFromLineWithLinks(String line, [Delta? delta]) { + final matches = linkRegExp.allMatches(line).toList(); + final nodeDelta = delta ?? Delta(); + + int lastUrlOffset = 0; + for (final match in matches) { + if (lastUrlOffset < match.start) { + nodeDelta.insert(line.substring(lastUrlOffset, match.start)); + } + + final link = line.substring(match.start, match.end); + nodeDelta.insert(link, attributes: {AppFlowyRichTextKeys.href: link}); + lastUrlOffset = match.end; + } + + final lastMatch = matches.last; + if (lastMatch.end - 1 < line.length - 1) { + nodeDelta.insert(line.substring(lastMatch.end)); + } + + return nodeDelta; + } + Node _codeBlockNodeFromMarkdown( String markdown, DeltaMarkdownDecoder decoder, @@ -149,9 +169,7 @@ class DocumentMarkdownDecoder extends Converter { // so this is treated like a normal paragraph if (!markdown.contains('\n') && markdown.split('`').length - 1 == markdown.length) { - return paragraphNode( - attributes: {'delta': decoder.convert(markdown).toJson()}, - ); + return paragraphNode(delta: decoder.convert(markdown)); } const codeMarker = '```'; int codeStartIndex = markdown.indexOf(codeMarker); @@ -162,9 +180,7 @@ class DocumentMarkdownDecoder extends Converter { // This if condition is for handling cases like ```\n` // In this case codeStartIndex = 0 and codeEndIndex = -1 if (codeEndIndex < codeStartIndex) { - return paragraphNode( - attributes: {'delta': decoder.convert(markdown).toJson()}, - ); + return paragraphNode(delta: decoder.convert(markdown)); } String codeBlock = markdown.substring( codeStartIndex + codeMarker.length, diff --git a/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index b46d66e0e..431571d73 100644 --- a/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/plugins/markdown/decoder/document_markdown_decoder.dart'; import 'package:flutter/widgets.dart'; int _textLengthOfNode(Node node) => node.delta?.length ?? 0; @@ -9,15 +10,27 @@ void _pasteSingleLine( String line, ) { assert(selection.isCollapsed); + + final markdownDecoder = DocumentMarkdownDecoder(); + final transaction = editorState.transaction; final node = editorState.getNodeAtPath(selection.end.path)!; - final transaction = editorState.transaction - ..insertText(node, selection.startIndex, line) - ..afterSelection = (Selection.collapsed( - Position( - path: selection.end.path, - offset: selection.startIndex + line.length, - ), - )); + + if (markdownDecoder.linkRegExp.hasMatch(line)) { + final delta = markdownDecoder.deltaFromLineWithLinks(line, node.delta); + transaction.insertNode( + node.path, + paragraphNode(delta: delta), + ); + } else { + transaction.insertText(node, selection.startIndex, line); + } + + transaction.afterSelection = Selection.collapsed( + Position( + path: selection.end.path, + offset: selection.startIndex + line.length, + ), + ); editorState.apply(transaction); }