diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart new file mode 100644 index 0000000000000..11dbb1b6a83f7 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart @@ -0,0 +1,65 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +// convert **abc** to bold abc. +ShortcutEventHandler doubleAsterisksToBold = (editorState, event) { + final selectionService = editorState.service.selectionService; + final selection = selectionService.currentSelection.value; + final textNodes = selectionService.currentSelectedNodes.whereType(); + if (selection == null || !selection.isSingle || textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final textNode = textNodes.first; + final text = textNode.toRawString().substring(0, selection.end.offset); + + // make sure the last two characters are **. + if (text.length < 2 || text[selection.end.offset - 1] != '*') { + return KeyEventResult.ignored; + } + + // find all the index of `*`. + final asteriskIndexes = []; + for (var i = 0; i < text.length; i++) { + if (text[i] == '*') { + asteriskIndexes.add(i); + } + } + + if (asteriskIndexes.length < 3) { + return KeyEventResult.ignored; + } + + // make sure the second to last and third to last asterisks are connected. + final thirdToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 3]; + final secondToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 2]; + final lastAsterisIndex = asteriskIndexes[asteriskIndexes.length - 1]; + if (secondToLastAsteriskIndex != thirdToLastAsteriskIndex + 1 || + lastAsterisIndex == secondToLastAsteriskIndex + 1) { + return KeyEventResult.ignored; + } + + // delete the last three asterisks. + // update the style of the text surround by `** **` to bold. + // and update the cursor position. + TransactionBuilder(editorState) + ..deleteText(textNode, lastAsterisIndex, 1) + ..deleteText(textNode, thirdToLastAsteriskIndex, 2) + ..formatText( + textNode, + thirdToLastAsteriskIndex, + selection.end.offset - thirdToLastAsteriskIndex - 2, + { + BuiltInAttributeKey.bold: true, + }, + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: selection.end.offset - 3, + ), + ) + ..commit(); + + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index eafee79a6d9cf..d0da879aabee3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_ke import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart'; @@ -243,4 +244,9 @@ List builtInShortcutEvents = [ command: 'page down', handler: pageDownHandler, ), + ShortcutEvent( + key: 'Double stars to bold', + command: 'shift+asterisk', + handler: doubleAsterisksToBold, + ), ]; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart index 5ad8ddfe3eddf..6d17c909e2696 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -139,6 +139,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.keyZ) { return PhysicalKeyboardKey.keyZ; } + if (this == LogicalKeyboardKey.asterisk) { + return PhysicalKeyboardKey.digit8; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart new file mode 100644 index 0000000000000..697bc80e83a93 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart @@ -0,0 +1,119 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('markdown_syntax_to_styled_text.dart', () { + group('convert double asterisks to bold', () { + Future insertAsterisk( + EditorWidgetTester editor, { + int repeat = 1, + }) async { + for (var i = 0; i < repeat; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.asterisk, + isShiftPressed: true, + ); + } + } + + testWidgets('**AppFlowy** to bold AppFlowy', (tester) async { + const text = '**AppFlowy*'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertAsterisk(editor); + final allBold = textNode.allSatisfyBoldInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: textNode.toRawString().length, + ), + ); + expect(allBold, true); + expect(textNode.toRawString(), 'AppFlowy'); + }); + + testWidgets('App**Flowy** to bold AppFlowy', (tester) async { + const text = 'App**Flowy*'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertAsterisk(editor); + final allBold = textNode.allSatisfyBoldInSelection( + Selection.single( + path: [0], + startOffset: 3, + endOffset: textNode.toRawString().length, + ), + ); + expect(allBold, true); + expect(textNode.toRawString(), 'AppFlowy'); + }); + + testWidgets('***AppFlowy** to bold *AppFlowy', (tester) async { + const text = '***AppFlowy*'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertAsterisk(editor); + final allBold = textNode.allSatisfyBoldInSelection( + Selection.single( + path: [0], + startOffset: 1, + endOffset: textNode.toRawString().length, + ), + ); + expect(allBold, true); + expect(textNode.toRawString(), '*AppFlowy'); + }); + + testWidgets('**** nothing changes', (tester) async { + const text = '***'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertAsterisk(editor); + final allBold = textNode.allSatisfyBoldInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: textNode.toRawString().length, + ), + ); + expect(allBold, false); + expect(textNode.toRawString(), text); + }); + }); + }); +}