Merge pull request #1187 from tekdel/main

feat: Support markdown to bold text
This commit is contained in:
Lucas.Xu 2022-10-08 11:46:20 +08:00 committed by GitHub
commit 8d6e1cdaa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 427 additions and 0 deletions

View File

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

View File

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

View File

@ -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_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';
@ -252,6 +253,16 @@ List<ShortcutEvent> builtInShortcutEvents = [
command: 'tab',
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(
key: 'Backquote to code',
command: 'backquote',

View File

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

View File

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