From c0964fad5d3b64c4d7b797ecfbfc6a9cfc2f9174 Mon Sep 17 00:00:00 2001 From: Andrei Dolgov Date: Mon, 26 Sep 2022 15:04:25 -0400 Subject: [PATCH 1/4] feat: double asterisks/underscores to bold text --- ...arkdown_syntax_to_styled_text_handler.dart | 131 ++++++++++ .../built_in_shortcut_events.dart | 11 + .../test/infra/test_raw_key_event.dart | 6 + ...wn_syntax_to_styled_text_handler_test.dart | 225 ++++++++++++++++++ 4 files changed, 373 insertions(+) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart new file mode 100644 index 0000000000..3656e4ddad --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart @@ -0,0 +1,131 @@ +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(); + 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 = []; + 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 - 2, + { + BuiltInAttributeKey.bold: 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(); + 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 = []; + 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 - 2, + { + BuiltInAttributeKey.bold: true, + }, + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: selection.end.offset - 3, + ), + ) + ..commit(); + editorState.editorStyle == EditorStyle.defaultStyle(); + + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 38eb9ee7c5..9ad249800d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/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_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/select_all_handler.dart'; @@ -249,4 +250,14 @@ List 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, + ), ]; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart index 5ad8ddfe3e..f9fb1c9920 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -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 gg; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart new file mode 100644 index 0000000000..d584a59886 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart @@ -0,0 +1,225 @@ +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 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('**** 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 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('____ 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); + }); + }); + }); +} From 6a902a2b218afcf0adc0e9cbf0a52864ecb93118 Mon Sep 17 00:00:00 2001 From: Andrei Dolgov Date: Mon, 26 Sep 2022 17:24:29 -0400 Subject: [PATCH 2/4] fix: workaround infinity formatting --- .../src/document/built_in_attribute_keys.dart | 1 + .../src/operation/transaction_builder.dart | 49 +++++++++--- ...arkdown_syntax_to_styled_text_handler.dart | 4 +- .../test/infra/test_raw_key_event.dart | 2 +- ...wn_syntax_to_styled_text_handler_test.dart | 78 +++++++++++++++++++ 5 files changed, 123 insertions(+), 11 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/built_in_attribute_keys.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/built_in_attribute_keys.dart index 0ed5440a9a..8cfe822f46 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/built_in_attribute_keys.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/built_in_attribute_keys.dart @@ -37,6 +37,7 @@ class BuiltInAttributeKey { static String checkbox = 'checkbox'; static String code = 'code'; static String number = 'number'; + static String defaultFormating = 'defaultFormating'; static List partialStyleKeys = [ BuiltInAttributeKey.bold, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index 7dfca6bcf7..79c2fe4b40 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'dart:math'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/document/attributes.dart'; import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/path.dart'; @@ -114,21 +115,17 @@ class TransactionBuilder { /// Inserts content at a specified index. /// Optionally, you may specify formatting attributes that are applied to the inserted string. - /// By default, the formatting attributes before the insert position will be used. + /// When no formatting attributes specified, the formating attributes before the insert position will be used if they don't have defaultFormatting flag set + /// When defaultFormatting flag is set before the insert position, it will be cleared. + /// When insert position is within a text having defaultFormatting flag set, the flag will be ignored and clear (formatting attributes of the text will be applied) insertText( TextNode node, int index, String content, { Attributes? attributes, }) { - var newAttributes = attributes; - if (index != 0 && attributes == null) { - newAttributes = - node.delta.slice(max(index - 1, 0), index).first.attributes; - if (newAttributes != null) { - newAttributes = Attributes.from(newAttributes); - } - } + final newAttributes = attributes ?? _getAttributesAt(node, index); + textEdit( node, () => Delta() @@ -227,4 +224,38 @@ class TransactionBuilder { afterSelection: afterSelection, ); } + + Attributes? _getAttributesAt(TextNode node, int index) { + if (index == 0) { + return null; + } + + final previousAttributes = + node.delta.slice(index - 1, index).first.attributes; + + final nextAttributes = node.delta.length > index + ? node.delta.slice(index, index + 1).first.attributes + : null; + + if (previousAttributes == null) { + return null; + } + + if (previousAttributes.containsKey(BuiltInAttributeKey.defaultFormating)) { + Attributes newAttributes = Map.from(previousAttributes) + ..removeWhere((key, _) => key == BuiltInAttributeKey.defaultFormating); + + if (node.previous != null) { + updateNode(node.next!, newAttributes); + if (previousAttributes == nextAttributes) { + updateNode(node.next!, newAttributes); + return newAttributes; + } + } + + return null; + } + + return Attributes.from(previousAttributes); + } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart index 3656e4ddad..298ee6128e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/extensions/path_extensions.dart'; import 'package:flutter/material.dart'; // convert **abc** to bold abc. @@ -51,6 +52,7 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) { selection.end.offset - thirdToLastAsteriskIndex - 2, { BuiltInAttributeKey.bold: true, + BuiltInAttributeKey.defaultFormating: true, }, ) ..afterSelection = Selection.collapsed( @@ -116,6 +118,7 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) { selection.end.offset - thirdToLastUnderscoreIndex - 2, { BuiltInAttributeKey.bold: true, + BuiltInAttributeKey.defaultFormating: true, }, ) ..afterSelection = Selection.collapsed( @@ -125,7 +128,6 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) { ), ) ..commit(); - editorState.editorStyle == EditorStyle.defaultStyle(); return KeyEventResult.handled; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart index f9fb1c9920..c9ff4e3222 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -143,7 +143,7 @@ extension on LogicalKeyboardKey { return PhysicalKeyboardKey.digit8; } if (this == LogicalKeyboardKey.underscore) { - return PhysicalKeyboardKey.minus gg; + return PhysicalKeyboardKey.minus; } throw UnimplementedError(); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart index d584a59886..c57e9fdd25 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart @@ -92,6 +92,45 @@ void main() async { expect(textNode.toRawString(), '*AppFlowy'); }); + testWidgets('**AppFlowy** application to bold AppFlowy only', + (tester) async { + const boldText = '**AppFlowy*'; + const normalText = ' application'; + 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); + for (var i = 0; i < normalText.length; i++) { + await editor.insertText( + textNode, normalText[i], i + boldText.length - 3); + } + final boldTextLength = boldText.replaceAll('*', '').length; + final appFlowyBold = textNode.allSatisfyBoldInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: boldTextLength, + ), + ); + final applicationNormal = textNode.allSatisfyBoldInSelection( + Selection.single( + path: [0], + startOffset: boldTextLength, + endOffset: textNode.toRawString().length, + ), + ); + expect(appFlowyBold, true); + expect(applicationNormal, false); + expect(textNode.toRawString(), 'AppFlowy application'); + }); + testWidgets('**** nothing changes', (tester) async { const text = '***'; final editor = tester.editor..insertTextNode(''); @@ -198,6 +237,45 @@ void main() async { expect(textNode.toRawString(), '_AppFlowy'); }); + testWidgets('__AppFlowy__ application to bold AppFlowy only', + (tester) async { + const boldText = '__AppFlowy_'; + const normalText = ' application'; + 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); + for (var i = 0; i < normalText.length; i++) { + await editor.insertText( + textNode, normalText[i], i + boldText.length - 3); + } + final boldTextLength = boldText.replaceAll('_', '').length; + final appFlowyBold = textNode.allSatisfyBoldInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: boldTextLength, + ), + ); + final applicationNormal = textNode.allSatisfyBoldInSelection( + Selection.single( + path: [0], + startOffset: boldTextLength, + endOffset: textNode.toRawString().length, + ), + ); + expect(appFlowyBold, true); + expect(applicationNormal, false); + expect(textNode.toRawString(), 'AppFlowy application'); + }); + testWidgets('____ nothing changes', (tester) async { const text = '___'; final editor = tester.editor..insertTextNode(''); From a8cb58cb4a696543f0296cbd6ca17cc980c8e18b Mon Sep 17 00:00:00 2001 From: Andrei Dolgov Date: Tue, 27 Sep 2022 16:38:18 -0400 Subject: [PATCH 3/4] fix: address a minor isssue when there is content after last asterisk/underscore --- .../markdown_syntax_to_styled_text_handler.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart index 298ee6128e..1f21cf0126 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart @@ -49,7 +49,7 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) { ..formatText( textNode, thirdToLastAsteriskIndex, - selection.end.offset - thirdToLastAsteriskIndex - 2, + selection.end.offset - thirdToLastAsteriskIndex - 3, { BuiltInAttributeKey.bold: true, BuiltInAttributeKey.defaultFormating: true, @@ -115,7 +115,7 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) { ..formatText( textNode, thirdToLastUnderscoreIndex, - selection.end.offset - thirdToLastUnderscoreIndex - 2, + selection.end.offset - thirdToLastUnderscoreIndex - 3, { BuiltInAttributeKey.bold: true, BuiltInAttributeKey.defaultFormating: true, From 1841fb293e5338390e5b496f96839420449ed289 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 8 Oct 2022 11:25:26 +0800 Subject: [PATCH 4/4] chore: temporarily remove the code that automatically formats when inserting text --- .../src/operation/transaction_builder.dart | 49 ++++--------------- ...arkdown_syntax_to_styled_text_handler.dart | 1 - ...wn_syntax_to_styled_text_handler_test.dart | 30 +----------- 3 files changed, 11 insertions(+), 69 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index 79c2fe4b40..7dfca6bcf7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -1,7 +1,6 @@ import 'dart:collection'; import 'dart:math'; -import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/document/attributes.dart'; import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/path.dart'; @@ -115,17 +114,21 @@ class TransactionBuilder { /// Inserts content at a specified index. /// Optionally, you may specify formatting attributes that are applied to the inserted string. - /// When no formatting attributes specified, the formating attributes before the insert position will be used if they don't have defaultFormatting flag set - /// When defaultFormatting flag is set before the insert position, it will be cleared. - /// When insert position is within a text having defaultFormatting flag set, the flag will be ignored and clear (formatting attributes of the text will be applied) + /// By default, the formatting attributes before the insert position will be used. insertText( TextNode node, int index, String content, { Attributes? attributes, }) { - final newAttributes = attributes ?? _getAttributesAt(node, index); - + var newAttributes = attributes; + if (index != 0 && attributes == null) { + newAttributes = + node.delta.slice(max(index - 1, 0), index).first.attributes; + if (newAttributes != null) { + newAttributes = Attributes.from(newAttributes); + } + } textEdit( node, () => Delta() @@ -224,38 +227,4 @@ class TransactionBuilder { afterSelection: afterSelection, ); } - - Attributes? _getAttributesAt(TextNode node, int index) { - if (index == 0) { - return null; - } - - final previousAttributes = - node.delta.slice(index - 1, index).first.attributes; - - final nextAttributes = node.delta.length > index - ? node.delta.slice(index, index + 1).first.attributes - : null; - - if (previousAttributes == null) { - return null; - } - - if (previousAttributes.containsKey(BuiltInAttributeKey.defaultFormating)) { - Attributes newAttributes = Map.from(previousAttributes) - ..removeWhere((key, _) => key == BuiltInAttributeKey.defaultFormating); - - if (node.previous != null) { - updateNode(node.next!, newAttributes); - if (previousAttributes == nextAttributes) { - updateNode(node.next!, newAttributes); - return newAttributes; - } - } - - return null; - } - - return Attributes.from(previousAttributes); - } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart index 1f21cf0126..37a45805ab 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart @@ -1,5 +1,4 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/extensions/path_extensions.dart'; import 'package:flutter/material.dart'; // convert **abc** to bold abc. diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart index c57e9fdd25..d60239ae49 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart @@ -95,7 +95,6 @@ void main() async { testWidgets('**AppFlowy** application to bold AppFlowy only', (tester) async { const boldText = '**AppFlowy*'; - const normalText = ' application'; final editor = tester.editor..insertTextNode(''); await editor.startTesting(); await editor.updateSelection( @@ -107,10 +106,6 @@ void main() async { await editor.insertText(textNode, boldText[i], i); } await insertAsterisk(editor); - for (var i = 0; i < normalText.length; i++) { - await editor.insertText( - textNode, normalText[i], i + boldText.length - 3); - } final boldTextLength = boldText.replaceAll('*', '').length; final appFlowyBold = textNode.allSatisfyBoldInSelection( Selection.single( @@ -119,16 +114,8 @@ void main() async { endOffset: boldTextLength, ), ); - final applicationNormal = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: boldTextLength, - endOffset: textNode.toRawString().length, - ), - ); expect(appFlowyBold, true); - expect(applicationNormal, false); - expect(textNode.toRawString(), 'AppFlowy application'); + expect(textNode.toRawString(), 'AppFlowy'); }); testWidgets('**** nothing changes', (tester) async { @@ -240,7 +227,6 @@ void main() async { testWidgets('__AppFlowy__ application to bold AppFlowy only', (tester) async { const boldText = '__AppFlowy_'; - const normalText = ' application'; final editor = tester.editor..insertTextNode(''); await editor.startTesting(); await editor.updateSelection( @@ -252,10 +238,6 @@ void main() async { await editor.insertText(textNode, boldText[i], i); } await insertUnderscore(editor); - for (var i = 0; i < normalText.length; i++) { - await editor.insertText( - textNode, normalText[i], i + boldText.length - 3); - } final boldTextLength = boldText.replaceAll('_', '').length; final appFlowyBold = textNode.allSatisfyBoldInSelection( Selection.single( @@ -264,16 +246,8 @@ void main() async { endOffset: boldTextLength, ), ); - final applicationNormal = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: boldTextLength, - endOffset: textNode.toRawString().length, - ), - ); expect(appFlowyBold, true); - expect(applicationNormal, false); - expect(textNode.toRawString(), 'AppFlowy application'); + expect(textNode.toRawString(), 'AppFlowy'); }); testWidgets('____ nothing changes', (tester) async {