Browse Source

Merge pull request #1143 from MrHeer/feat/markdown_syntax_to_code_text

feat: backquote to code text
Lucas.Xu 2 years ago
parent
commit
c0df0badec

+ 5 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart

@@ -54,6 +54,11 @@ extension TextNodeExtension on TextNode {
         return value == true;
       });
 
+  bool allSatisfyCodeInSelection(Selection selection) =>
+      allSatisfyInSelection(selection, BuiltInAttributeKey.code, (value) {
+        return value == true;
+      });
+
   bool allSatisfyInSelection(
     Selection selection,
     String styleKey,

+ 126 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart

@@ -0,0 +1,126 @@
+import "dart:math";
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
+import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
+import 'package:flutter/material.dart';
+
+bool _isCodeStyle(TextNode textNode, int index) {
+  return textNode.allSatisfyCodeInSelection(Selection.single(
+      path: textNode.path, startOffset: index, endOffset: index + 1));
+}
+
+// enter escape mode when start two backquote
+bool _isEscapeBackquote(String text, List<int> backquoteIndexes) {
+  if (backquoteIndexes.length >= 2) {
+    final firstBackquoteIndex = backquoteIndexes[0];
+    final secondBackquoteIndex = backquoteIndexes[1];
+    return firstBackquoteIndex == secondBackquoteIndex - 1;
+  }
+  return false;
+}
+
+// find all the index of `, exclusion in code style.
+List<int> _findBackquoteIndexes(String text, TextNode textNode) {
+  final backquoteIndexes = <int>[];
+  for (var i = 0; i < text.length; i++) {
+    if (text[i] == '`' && _isCodeStyle(textNode, i) == false) {
+      backquoteIndexes.add(i);
+    }
+  }
+  return backquoteIndexes;
+}
+
+/// To denote a word or phrase as code, enclose it in backticks (`).
+/// If the word or phrase you want to denote as code includes one or more
+/// backticks, you can escape it by enclosing the word or phrase in double
+/// backticks (``).
+ShortcutEventHandler backquoteToCodeHandler = (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 selectionText = textNode
+      .toRawString()
+      .substring(selection.start.offset, selection.end.offset);
+
+  // toggle code style when selected some text
+  if (selectionText.length > 0) {
+    formatEmbedCode(editorState);
+    return KeyEventResult.handled;
+  }
+
+  final text = textNode.toRawString().substring(0, selection.end.offset);
+  final backquoteIndexes = _findBackquoteIndexes(text, textNode);
+  if (backquoteIndexes.isEmpty) {
+    return KeyEventResult.ignored;
+  }
+
+  final endIndex = selection.end.offset;
+
+  if (_isEscapeBackquote(text, backquoteIndexes)) {
+    final firstBackquoteIndex = backquoteIndexes[0];
+    final secondBackquoteIndex = backquoteIndexes[1];
+    final lastBackquoteIndex = backquoteIndexes[backquoteIndexes.length - 1];
+    if (secondBackquoteIndex == lastBackquoteIndex ||
+        secondBackquoteIndex == lastBackquoteIndex - 1 ||
+        lastBackquoteIndex != endIndex - 1) {
+      // ``(`),```(`),``...`...(`) should ignored
+      return KeyEventResult.ignored;
+    }
+
+    TransactionBuilder(editorState)
+      ..deleteText(textNode, lastBackquoteIndex, 1)
+      ..deleteText(textNode, firstBackquoteIndex, 2)
+      ..formatText(
+        textNode,
+        firstBackquoteIndex,
+        endIndex - firstBackquoteIndex - 3,
+        {
+          BuiltInAttributeKey.code: true,
+        },
+      )
+      ..afterSelection = Selection.collapsed(
+        Position(
+          path: textNode.path,
+          offset: endIndex - 3,
+        ),
+      )
+      ..commit();
+
+    return KeyEventResult.handled;
+  }
+
+  // handle single backquote
+  final startIndex = backquoteIndexes[0];
+  if (startIndex == endIndex - 1) {
+    return KeyEventResult.ignored;
+  }
+
+  // delete the backquote.
+  // update the style of the text surround by ` ` to code.
+  // and update the cursor position.
+  TransactionBuilder(editorState)
+    ..deleteText(textNode, startIndex, 1)
+    ..formatText(
+      textNode,
+      startIndex,
+      endIndex - startIndex - 1,
+      {
+        BuiltInAttributeKey.code: true,
+      },
+    )
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: endIndex - 1,
+      ),
+    )
+    ..commit();
+
+  return KeyEventResult.handled;
+};

+ 6 - 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/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';
@@ -251,6 +252,11 @@ List<ShortcutEvent> builtInShortcutEvents = [
     command: 'tab',
     handler: tabHandler,
   ),
+  ShortcutEvent(
+    key: 'Backquote to code',
+    command: 'backquote',
+    handler: backquoteToCodeHandler,
+  ),
   // https://github.com/flutter/flutter/issues/104944
   // Workaround: Using space editing on the web platform often results in errors,
   //  so adding a shortcut event to handle the space input instead of using the

+ 154 - 0
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart

@@ -0,0 +1,154 @@
+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 single backquote to code', () {
+      Future<void> insertBackquote(
+        EditorWidgetTester editor, {
+        int repeat = 1,
+      }) async {
+        for (var i = 0; i < repeat; i++) {
+          await editor.pressLogicKey(
+            LogicalKeyboardKey.backquote,
+          );
+        }
+      }
+
+      testWidgets('`AppFlowy` to code 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 insertBackquote(editor);
+        final allCode = textNode.allSatisfyCodeInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 0,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allCode, true);
+        expect(textNode.toRawString(), 'AppFlowy');
+      });
+
+      testWidgets('App`Flowy` to code 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 insertBackquote(editor);
+        final allCode = textNode.allSatisfyCodeInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 3,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allCode, 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 insertBackquote(editor);
+        final allCode = textNode.allSatisfyCodeInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 0,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allCode, false);
+        expect(textNode.toRawString(), text);
+      });
+    });
+
+    group('convert double backquote to code', () {
+      Future<void> insertBackquote(
+        EditorWidgetTester editor, {
+        int repeat = 1,
+      }) async {
+        for (var i = 0; i < repeat; i++) {
+          await editor.pressLogicKey(
+            LogicalKeyboardKey.backquote,
+          );
+        }
+      }
+
+      testWidgets('```AppFlowy`` to code `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 insertBackquote(editor);
+        final allCode = textNode.allSatisfyCodeInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 1,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allCode, 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 insertBackquote(editor);
+        final allCode = textNode.allSatisfyCodeInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 0,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allCode, false);
+        expect(textNode.toRawString(), text);
+      });
+    });
+  });
+}