feat: double hyphen to em dash (#395)
* feat: double hyphen to em dash * chore: fix tests --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
parent
1f2668c18a
commit
2eb801e7f5
7 changed files with 160 additions and 6 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
/// insert divider into a document by typing three minuses(-).
|
||||
/// insert divider into a document by typing three dashes (-)
|
||||
/// or one em dash (—) and one dash (-)
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ export 'format_single_character/format_strikethrough.dart';
|
|||
export 'format_double_characters/format_double_characters.dart';
|
||||
export 'format_double_characters/format_bold.dart';
|
||||
export 'format_double_characters/format_strikethrough.dart';
|
||||
export 'format_double_characters/format_double_hyphen.dart';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
const _hyphen = '-';
|
||||
const _emDash = '—'; // This is an em dash — not a single dash - !!
|
||||
|
||||
/// format two hyphens into an em dash
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - mobile
|
||||
/// - web
|
||||
///
|
||||
final CharacterShortcutEvent formatDoubleHyphenEmDash = CharacterShortcutEvent(
|
||||
key: 'format double hyphen into an em dash',
|
||||
character: _hyphen,
|
||||
handler: (editorState) async => handleDoubleCharacterReplacement(
|
||||
editorState: editorState,
|
||||
character: _hyphen,
|
||||
replacement: _emDash,
|
||||
),
|
||||
);
|
||||
|
||||
Future<bool> handleDoubleCharacterReplacement({
|
||||
required EditorState editorState,
|
||||
required String character,
|
||||
required String replacement,
|
||||
}) async {
|
||||
assert(character.length == 1);
|
||||
|
||||
Selection? selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!selection.isCollapsed) {
|
||||
await editorState.deleteSelection(selection);
|
||||
}
|
||||
|
||||
selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null || delta.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selection.end.offset > 0) {
|
||||
final plain = delta.toPlainText();
|
||||
|
||||
final previousCharacter = plain[selection.end.offset - 1];
|
||||
if (previousCharacter != _hyphen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final replace = editorState.transaction
|
||||
..replaceText(
|
||||
node,
|
||||
selection.end.offset - 1,
|
||||
1,
|
||||
_emDash,
|
||||
);
|
||||
|
||||
await editorState.apply(replace);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ bool handleFormatByWrappingWithSingleCharacter({
|
|||
// Determine if a 'Character' already exists in the node and only once.
|
||||
// 1. This is no 'Character' in the plainText: indexOf returns -1.
|
||||
// 2. More than one 'Character' in the plainText: the headCharIndex and endCharIndex are supposed to be the same, if not, which means plainText has more than one character. For example: when plainText is '_abc', it will trigger formatting(remind:the last char is used to trigger the formatting,so it won't be counted in the plainText.). But adding '_' after 'a__ab' won't trigger formatting.
|
||||
// 3. there're two characters connecting together, like adding '_' after 'abc_' won't trigger formatting.
|
||||
// 3. there are two characters connecting together, like adding '_' after 'abc_' won't trigger formatting.
|
||||
if (headCharIndex == -1 ||
|
||||
headCharIndex != endCharIndex ||
|
||||
headCharIndex == selection.end.offset - 1) {
|
||||
|
|
|
|||
|
|
@ -29,4 +29,7 @@ final List<CharacterShortcutEvent> markdownSyntaxShortcutEvents = [
|
|||
// format bold, **bold** or __bold__
|
||||
formatDoubleAsterisksToBold,
|
||||
formatDoubleUnderscoresToBold,
|
||||
|
||||
// format -- into em dash
|
||||
formatDoubleHyphenEmDash,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../../infra/testable_editor.dart';
|
||||
|
||||
const _hyphen = '-';
|
||||
const _emDash = '—'; // This is an em dash
|
||||
|
||||
void main() async {
|
||||
group('format_double_hyphen.dart', () {
|
||||
testWidgets('two dashes to em dash', (tester) async {
|
||||
final editor = tester.editor..addEmptyParagraph();
|
||||
await editor.startTesting();
|
||||
|
||||
await editor.updateSelection(Selection.collapsed(Position(path: [0])));
|
||||
|
||||
await editor.ime.typeText(_hyphen);
|
||||
await editor.ime.typeText(_hyphen);
|
||||
|
||||
final delta = editor.nodeAtPath([0])!.delta!;
|
||||
expect(delta.length, 1);
|
||||
expect(delta.toPlainText(), _emDash);
|
||||
|
||||
await editor.dispose();
|
||||
});
|
||||
|
||||
testWidgets('two dashes to em dash with selection', (tester) async {
|
||||
const welcome = 'Welcome';
|
||||
const initialText = '$_hyphen$welcome';
|
||||
|
||||
final editor = tester.editor..addParagraph(initialText: initialText);
|
||||
await editor.startTesting();
|
||||
|
||||
await editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 1,
|
||||
endOffset: initialText.length,
|
||||
),
|
||||
);
|
||||
|
||||
await editor.ime.typeText(_hyphen);
|
||||
|
||||
final delta = editor.nodeAtPath([0])!.delta!;
|
||||
expect(delta.length, 1);
|
||||
expect(delta.toPlainText(), _emDash);
|
||||
|
||||
await editor.dispose();
|
||||
});
|
||||
|
||||
testWidgets('em dash + dash to divider', (tester) async {
|
||||
final editor = tester.editor..addParagraph(initialText: _emDash);
|
||||
await editor.startTesting();
|
||||
|
||||
await editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
|
||||
await editor.ime.typeText(_hyphen);
|
||||
|
||||
final node = editor.nodeAtPath([0]);
|
||||
expect(node!.type, DividerBlockKeys.type);
|
||||
|
||||
await editor.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -315,14 +315,24 @@ class MockIMEInput {
|
|||
}
|
||||
|
||||
Future<void> replaceText(String text) async {
|
||||
final selection = editorState.selection?.normalized;
|
||||
if (selection == null || selection.isCollapsed) {
|
||||
var selection = editorState.selection?.normalized;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
final texts = editorState.getTextInSelection(selection).join('\n');
|
||||
if (!selection.isSingle || !selection.isCollapsed) {
|
||||
await editorState.deleteSelection(selection);
|
||||
}
|
||||
selection = editorState.selection?.normalized;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final oldText =
|
||||
editorState.getNodeAtPath(selection.start.path)!.delta!.toPlainText();
|
||||
|
||||
await imeInput.apply([
|
||||
TextEditingDeltaReplacement(
|
||||
oldText: ' $texts',
|
||||
oldText: ' $oldText',
|
||||
replacementText: text,
|
||||
replacedRange: TextSelection(
|
||||
baseOffset: selection.startIndex + 1,
|
||||
|
|
|
|||
Loading…
Reference in a new issue