From de94a5864fb53f26aac3d2ca7480b2cc5637f3fd Mon Sep 17 00:00:00 2001 From: Jazim Akbar <61325401+jazima@users.noreply.github.com> Date: Mon, 1 Jan 2024 01:34:33 -0500 Subject: [PATCH] feat: add markdown link syntax formatting (#618) * Add markdown link syntax formatting * chore: format code --------- Co-authored-by: Lucas.Xu --- .../character/character_shortcut_events.dart | 1 + .../markdown_link_shortcut_event.dart | 63 ++++++++++++ ...down_syntax_character_shortcut_events.dart | 3 + .../format_markdown_link_test.dart | 96 +++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 lib/src/editor/editor_component/service/shortcuts/character/markdown_link_shortcut_event.dart create mode 100644 test/new/service/shortcuts/character_shortcut_events/markdown_shortcut_events/format_markdown_link_test.dart diff --git a/lib/src/editor/editor_component/service/shortcuts/character/character_shortcut_events.dart b/lib/src/editor/editor_component/service/shortcuts/character/character_shortcut_events.dart index a2e0b1ef1..ea2978552 100644 --- a/lib/src/editor/editor_component/service/shortcuts/character/character_shortcut_events.dart +++ b/lib/src/editor/editor_component/service/shortcuts/character/character_shortcut_events.dart @@ -8,5 +8,6 @@ export 'format_single_character/format_italic.dart'; export 'format_single_character/format_single_character.dart'; export 'format_single_character/format_strikethrough.dart'; export 'insert_newline.dart'; +export 'markdown_link_shortcut_event.dart'; export 'markdown_syntax_character_shortcut_events.dart'; export 'slash_command.dart'; diff --git a/lib/src/editor/editor_component/service/shortcuts/character/markdown_link_shortcut_event.dart b/lib/src/editor/editor_component/service/shortcuts/character/markdown_link_shortcut_event.dart new file mode 100644 index 000000000..8405463b6 --- /dev/null +++ b/lib/src/editor/editor_component/service/shortcuts/character/markdown_link_shortcut_event.dart @@ -0,0 +1,63 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; + +/// format the markdown link syntax to hyperlink +final CharacterShortcutEvent formatMarkdownLinkToLink = CharacterShortcutEvent( + key: 'format the text surrounded by double asterisks to bold', + character: ')', + handler: (editorState) async => handleFormatMarkdownLinkToLink( + editorState: editorState, + ), +); + +final _linkRegex = RegExp(r'\[([^\]]*)\]\((.*?)\)'); + +bool handleFormatMarkdownLinkToLink({ + required EditorState editorState, +}) { + final selection = editorState.selection; + // if the selection is not collapsed or the cursor is at the first 5 index range, we don't need to format it. + // we should return false to let the IME handle it. + if (selection == null || !selection.isCollapsed || selection.end.offset < 6) { + return false; + } + + final path = selection.end.path; + final node = editorState.getNodeAtPath(path); + final delta = node?.delta; + // if the node doesn't contain the delta(which means it isn't a text) + // we don't need to format it. + if (node == null || delta == null) { + return false; + } + + final plainText = '${delta.toPlainText()})'; + + // Determine if regex matches the plainText. + if (!_linkRegex.hasMatch(plainText)) { + return false; + } + + final matches = _linkRegex.allMatches(plainText); + final lastMatch = matches.last; + final title = lastMatch.group(1); + final link = lastMatch.group(2); + + // if all the conditions are met, we should format the text to a link. + final transaction = editorState.transaction + ..deleteText( + node, + lastMatch.start, + lastMatch.end - lastMatch.start - 1, + ) + ..insertText( + node, + lastMatch.start, + title!, + attributes: { + AppFlowyRichTextKeys.href: link, + }, + ); + editorState.apply(transaction); + + return true; +} diff --git a/lib/src/editor/editor_component/service/shortcuts/character/markdown_syntax_character_shortcut_events.dart b/lib/src/editor/editor_component/service/shortcuts/character/markdown_syntax_character_shortcut_events.dart index edc95c5ef..9f7dc59cf 100644 --- a/lib/src/editor/editor_component/service/shortcuts/character/markdown_syntax_character_shortcut_events.dart +++ b/lib/src/editor/editor_component/service/shortcuts/character/markdown_syntax_character_shortcut_events.dart @@ -32,4 +32,7 @@ final List markdownSyntaxShortcutEvents = [ // format -- into em dash formatDoubleHyphenEmDash, + + // format [*](*) to link + formatMarkdownLinkToLink, ]; diff --git a/test/new/service/shortcuts/character_shortcut_events/markdown_shortcut_events/format_markdown_link_test.dart b/test/new/service/shortcuts/character_shortcut_events/markdown_shortcut_events/format_markdown_link_test.dart new file mode 100644 index 000000000..1d1d3cc7c --- /dev/null +++ b/test/new/service/shortcuts/character_shortcut_events/markdown_shortcut_events/format_markdown_link_test.dart @@ -0,0 +1,96 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../../util/util.dart'; + +void main() async { + group('format the text in markdown link syntax to appflowy href', () { + // Before + // [AppFlowy](appflowy.com| + // After + // [href:appflowy.com]AppFlowy + test('[AppFlowy](appflowy.com) to format AppFlowy as link', () async { + const text = 'AppFlowy'; + const link = 'appflowy.com'; + final document = Document.blank().addParagraphs( + 1, + builder: (index) => Delta()..insert('[$text]($link'), + ); + + final editorState = EditorState(document: document); + + // add cursor in the end of the text + final selection = Selection.collapsed( + Position(path: [0], offset: text.length + 1), + ); + editorState.selection = selection; + // run targeted CharacterShortcutEvent + final result = await formatMarkdownLinkToLink.execute(editorState); + + expect(result, true); + final after = editorState.getNodeAtPath([0])!; + expect(after.delta!.toPlainText(), text); + expect( + after.delta!.toList()[0].attributes, + {AppFlowyRichTextKeys.href: link}, + ); + }); + + // Before + // App[Flowy](flowy.com| + // After + // App[href:appflowy.com]Flowy + test('App[Flowy](appflowy.com) to App[href:appflowy.com]Flowy', () async { + const text1 = 'App'; + const text2 = 'Flowy'; + const link = 'appflowy.com'; + final document = Document.blank().addParagraphs( + 1, + builder: (index) => Delta()..insert('$text1[$text2]($link'), + ); + + final editorState = EditorState(document: document); + + final selection = Selection.collapsed( + Position(path: [0], offset: text1.length + text2.length + 1), + ); + editorState.selection = selection; + + final result = await formatMarkdownLinkToLink.execute(editorState); + + expect(result, true); + final after = editorState.getNodeAtPath([0])!; + expect(after.delta!.toPlainText(), '$text1$text2'); + expect(after.delta!.toList()[0].attributes, null); + expect( + after.delta!.toList()[1].attributes, + {AppFlowyRichTextKeys.href: link}, + ); + }); + + // Before + // AppFlowy[](| + // After + // AppFlowy[]()| + test('empty text change nothing', () async { + const text = 'AppFlowy[]('; + final document = Document.blank().addParagraphs( + 1, + builder: (index) => Delta()..insert(text), + ); + + final editorState = EditorState(document: document); + + final selection = Selection.collapsed( + Position(path: [0], offset: text.length), + ); + editorState.selection = selection; + + final result = await formatTildeToStrikethrough.execute(editorState); + + expect(result, false); + final after = editorState.getNodeAtPath([0])!; + expect(after.delta!.toPlainText(), text); + }); + }); +}