Skip to content

Commit

Permalink
feat: add markdown link syntax formatting (#618)
Browse files Browse the repository at this point in the history
* Add markdown link syntax formatting

* chore: format code

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
  • Loading branch information
jazima and LucasXu0 committed Jan 1, 2024
1 parent d15737f commit de94a58
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ final List<CharacterShortcutEvent> markdownSyntaxShortcutEvents = [

// format -- into em dash
formatDoubleHyphenEmDash,

// format [*](*) to link
formatMarkdownLinkToLink,
];
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);
});
});
}

0 comments on commit de94a58

Please sign in to comment.