ソースを参照

Merge pull request #1187 from tekdel/main

feat: Support markdown to bold text
Lucas.Xu 2 年 前
コミット
8d6e1cdaa1

+ 1 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/document/built_in_attribute_keys.dart

@@ -37,6 +37,7 @@ class BuiltInAttributeKey {
   static String checkbox = 'checkbox';
   static String checkbox = 'checkbox';
   static String code = 'code';
   static String code = 'code';
   static String number = 'number';
   static String number = 'number';
+  static String defaultFormating = 'defaultFormating';
 
 
   static List<String> partialStyleKeys = [
   static List<String> partialStyleKeys = [
     BuiltInAttributeKey.bold,
     BuiltInAttributeKey.bold,

+ 132 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart

@@ -0,0 +1,132 @@
+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<TextNode>();
+  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 = <int>[];
+  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 - 3,
+      {
+        BuiltInAttributeKey.bold: true,
+        BuiltInAttributeKey.defaultFormating: true,
+      },
+    )
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: selection.end.offset - 3,
+      ),
+    )
+    ..commit();
+
+  return KeyEventResult.handled;
+};
+
+// convert __abc__ to bold abc.
+ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
+  final selectionService = editorState.service.selectionService;
+  final selection = selectionService.currentSelection.value;
+  final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
+  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 underscoreIndexes = <int>[];
+  for (var i = 0; i < text.length; i++) {
+    if (text[i] == '_') {
+      underscoreIndexes.add(i);
+    }
+  }
+
+  if (underscoreIndexes.length < 3) {
+    return KeyEventResult.ignored;
+  }
+
+  // make sure the second to last and third to last underscores are connected.
+  final thirdToLastUnderscoreIndex =
+      underscoreIndexes[underscoreIndexes.length - 3];
+  final secondToLastUnderscoreIndex =
+      underscoreIndexes[underscoreIndexes.length - 2];
+  final lastAsterisIndex = underscoreIndexes[underscoreIndexes.length - 1];
+  if (secondToLastUnderscoreIndex != thirdToLastUnderscoreIndex + 1 ||
+      lastAsterisIndex == secondToLastUnderscoreIndex + 1) {
+    return KeyEventResult.ignored;
+  }
+
+  // delete the last three underscores.
+  // update the style of the text surround by `__ __` to bold.
+  // and update the cursor position.
+  TransactionBuilder(editorState)
+    ..deleteText(textNode, lastAsterisIndex, 1)
+    ..deleteText(textNode, thirdToLastUnderscoreIndex, 2)
+    ..formatText(
+      textNode,
+      thirdToLastUnderscoreIndex,
+      selection.end.offset - thirdToLastUnderscoreIndex - 3,
+      {
+        BuiltInAttributeKey.bold: true,
+        BuiltInAttributeKey.defaultFormating: true,
+      },
+    )
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: selection.end.offset - 3,
+      ),
+    )
+    ..commit();
+
+  return KeyEventResult.handled;
+};

+ 11 - 0
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/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/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/enter_without_shift_in_text_node_handler.dart';
+import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_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/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/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/redo_undo_handler.dart';
@@ -252,6 +253,16 @@ List<ShortcutEvent> builtInShortcutEvents = [
     command: 'tab',
     command: 'tab',
     handler: tabHandler,
     handler: tabHandler,
   ),
   ),
+  ShortcutEvent(
+    key: 'Double stars to bold',
+    command: 'shift+asterisk',
+    handler: doubleAsterisksToBold,
+  ),
+  ShortcutEvent(
+    key: 'Double underscores to bold',
+    command: 'shift+underscore',
+    handler: doubleUnderscoresToBold,
+  ),
   ShortcutEvent(
   ShortcutEvent(
     key: 'Backquote to code',
     key: 'Backquote to code',
     command: 'backquote',
     command: 'backquote',

+ 6 - 0
frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart

@@ -139,6 +139,12 @@ extension on LogicalKeyboardKey {
     if (this == LogicalKeyboardKey.keyZ) {
     if (this == LogicalKeyboardKey.keyZ) {
       return PhysicalKeyboardKey.keyZ;
       return PhysicalKeyboardKey.keyZ;
     }
     }
+    if (this == LogicalKeyboardKey.asterisk) {
+      return PhysicalKeyboardKey.digit8;
+    }
+    if (this == LogicalKeyboardKey.underscore) {
+      return PhysicalKeyboardKey.minus;
+    }
     if (this == LogicalKeyboardKey.tilde) {
     if (this == LogicalKeyboardKey.tilde) {
       return PhysicalKeyboardKey.backquote;
       return PhysicalKeyboardKey.backquote;
     }
     }

+ 277 - 0
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart

@@ -0,0 +1,277 @@
+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_handler.dart', () {
+    group('convert double asterisks to bold', () {
+      Future<void> 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('**AppFlowy** application to bold AppFlowy only',
+          (tester) async {
+        const boldText = '**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 < boldText.length; i++) {
+          await editor.insertText(textNode, boldText[i], i);
+        }
+        await insertAsterisk(editor);
+        final boldTextLength = boldText.replaceAll('*', '').length;
+        final appFlowyBold = textNode.allSatisfyBoldInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 0,
+            endOffset: boldTextLength,
+          ),
+        );
+        expect(appFlowyBold, 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);
+      });
+    });
+
+    group('convert double underscores to bold', () {
+      Future<void> insertUnderscore(
+        EditorWidgetTester editor, {
+        int repeat = 1,
+      }) async {
+        for (var i = 0; i < repeat; i++) {
+          await editor.pressLogicKey(
+            LogicalKeyboardKey.underscore,
+            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 insertUnderscore(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 insertUnderscore(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 insertUnderscore(editor);
+        final allBold = textNode.allSatisfyBoldInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 1,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allBold, true);
+        expect(textNode.toRawString(), '_AppFlowy');
+      });
+
+      testWidgets('__AppFlowy__ application to bold AppFlowy only',
+          (tester) async {
+        const boldText = '__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 < boldText.length; i++) {
+          await editor.insertText(textNode, boldText[i], i);
+        }
+        await insertUnderscore(editor);
+        final boldTextLength = boldText.replaceAll('_', '').length;
+        final appFlowyBold = textNode.allSatisfyBoldInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 0,
+            endOffset: boldTextLength,
+          ),
+        );
+        expect(appFlowyBold, 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 insertUnderscore(editor);
+        final allBold = textNode.allSatisfyBoldInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 0,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allBold, false);
+        expect(textNode.toRawString(), text);
+      });
+    });
+  });
+}