mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #1143 from MrHeer/feat/markdown_syntax_to_code_text
feat: backquote to code text
This commit is contained in:
commit
c0df0badec
@ -54,6 +54,11 @@ extension TextNodeExtension on TextNode {
|
|||||||
return value == true;
|
return value == true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool allSatisfyCodeInSelection(Selection selection) =>
|
||||||
|
allSatisfyInSelection(selection, BuiltInAttributeKey.code, (value) {
|
||||||
|
return value == true;
|
||||||
|
});
|
||||||
|
|
||||||
bool allSatisfyInSelection(
|
bool allSatisfyInSelection(
|
||||||
Selection selection,
|
Selection selection,
|
||||||
String styleKey,
|
String styleKey,
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -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.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';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_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',
|
command: 'tab',
|
||||||
handler: tabHandler,
|
handler: tabHandler,
|
||||||
),
|
),
|
||||||
|
ShortcutEvent(
|
||||||
|
key: 'Backquote to code',
|
||||||
|
command: 'backquote',
|
||||||
|
handler: backquoteToCodeHandler,
|
||||||
|
),
|
||||||
// https://github.com/flutter/flutter/issues/104944
|
// https://github.com/flutter/flutter/issues/104944
|
||||||
// Workaround: Using space editing on the web platform often results in errors,
|
// 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
|
// so adding a shortcut event to handle the space input instead of using the
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user