-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add markdown link syntax formatting (#618)
* Add markdown link syntax formatting * chore: format code --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
- Loading branch information
Showing
4 changed files
with
163 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
...src/editor/editor_component/service/shortcuts/character/markdown_link_shortcut_event.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
...ortcuts/character_shortcut_events/markdown_shortcut_events/format_markdown_link_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
} |