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:
Mathias Mogensen 2023-08-21 05:46:45 +02:00 • committed by GitHub
commit 2eb801e7f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 6 deletions

View file

@ -1,6 +1,7 @@
import 'package:appflowy_editor/appflowy_editor.dart'; 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 /// - support
/// - desktop /// - desktop

View file

@ -10,3 +10,4 @@ export 'format_single_character/format_strikethrough.dart';
export 'format_double_characters/format_double_characters.dart'; export 'format_double_characters/format_double_characters.dart';
export 'format_double_characters/format_bold.dart'; export 'format_double_characters/format_bold.dart';
export 'format_double_characters/format_strikethrough.dart'; export 'format_double_characters/format_strikethrough.dart';
export 'format_double_characters/format_double_hyphen.dart';

View file

@ -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;
}

View file

@ -37,7 +37,7 @@ bool handleFormatByWrappingWithSingleCharacter({
// Determine if a 'Character' already exists in the node and only once. // Determine if a 'Character' already exists in the node and only once.
// 1. This is no 'Character' in the plainText: indexOf returns -1. // 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. // 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 || if (headCharIndex == -1 ||
headCharIndex != endCharIndex || headCharIndex != endCharIndex ||
headCharIndex == selection.end.offset - 1) { headCharIndex == selection.end.offset - 1) {

View file

@ -29,4 +29,7 @@ final List<CharacterShortcutEvent> markdownSyntaxShortcutEvents = [
// format bold, **bold** or __bold__ // format bold, **bold** or __bold__
formatDoubleAsterisksToBold, formatDoubleAsterisksToBold,
formatDoubleUnderscoresToBold, formatDoubleUnderscoresToBold,
// format -- into em dash
formatDoubleHyphenEmDash,
]; ];

View file

@ -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();
});
});
}

View file

@ -315,14 +315,24 @@ class MockIMEInput {
} }
Future<void> replaceText(String text) async { Future<void> replaceText(String text) async {
final selection = editorState.selection?.normalized; var selection = editorState.selection?.normalized;
if (selection == null || selection.isCollapsed) { if (selection == null) {
return; 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([ await imeInput.apply([
TextEditingDeltaReplacement( TextEditingDeltaReplacement(
oldText: ' $texts', oldText: ' $oldText',
replacementText: text, replacementText: text,
replacedRange: TextSelection( replacedRange: TextSelection(
baseOffset: selection.startIndex + 1, baseOffset: selection.startIndex + 1,