feat: implement converting 1. to numbered list style
This commit is contained in:
parent
6df42d4b8a
commit
f624f3a308
11 changed files with 377 additions and 149 deletions
|
|
@ -84,6 +84,9 @@ class SimpleEditor extends StatelessWidget {
|
|||
formatAsteriskToBulletedList,
|
||||
formatMinusToBulletedList,
|
||||
|
||||
// numbered list
|
||||
formatNumberToNumberedList,
|
||||
|
||||
// slash
|
||||
slashCommand,
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ class Position {
|
|||
this.offset = 0,
|
||||
});
|
||||
|
||||
Position.invalid()
|
||||
: path = [-1],
|
||||
offset = -1;
|
||||
|
||||
factory Position.fromJson(Map<String, dynamic> json) {
|
||||
final path = Path.from(json['path'] as List);
|
||||
final offset = json['offset'];
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ class Selection {
|
|||
: start = position,
|
||||
end = position;
|
||||
|
||||
Selection.invalid()
|
||||
: start = Position.invalid(),
|
||||
end = Position.invalid();
|
||||
|
||||
final Position start;
|
||||
final Position end;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
/// Formats the current node to specified markdown style.
|
||||
///
|
||||
/// For example,
|
||||
/// bulleted list: '- '
|
||||
/// numbered list: '1. '
|
||||
/// quote: '> '
|
||||
/// ...
|
||||
Future<bool> formatMarkdownSymbol(
|
||||
EditorState editorState,
|
||||
bool Function(Node node) shouldFormat,
|
||||
bool Function(
|
||||
String text,
|
||||
Selection selection,
|
||||
)
|
||||
predicate,
|
||||
Node Function(
|
||||
String text,
|
||||
Node node,
|
||||
Delta delta,
|
||||
)
|
||||
nodeBuilder,
|
||||
) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final position = selection.end;
|
||||
final node = editorState.getNodeAtPath(position.path);
|
||||
|
||||
if (node == null || !shouldFormat(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the text from the start of the document until the selection.
|
||||
final delta = node.delta;
|
||||
if (delta == null) {
|
||||
return false;
|
||||
}
|
||||
final text = delta.toPlainText().substring(0, selection.end.offset);
|
||||
|
||||
// If the text doesn't match the predicate, then we don't want to
|
||||
// format it.
|
||||
if (!predicate(text, selection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: node.path,
|
||||
offset: 0,
|
||||
),
|
||||
);
|
||||
|
||||
final formattedNode = nodeBuilder(text, node, delta);
|
||||
|
||||
// Create a transaction that replaces the current node with the
|
||||
// formatted node.
|
||||
final transaction = editorState.transaction
|
||||
..insertNode(
|
||||
node.path,
|
||||
formattedNode,
|
||||
)
|
||||
..deleteNode(node)
|
||||
..afterSelection = afterSelection;
|
||||
|
||||
await editorState.apply(transaction);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ export 'bulleted_list_block_component/bulleted_list_character_shortcut.dart';
|
|||
|
||||
// numbered list
|
||||
export 'numbered_list_block_component/numbered_list_block_component.dart';
|
||||
export 'numbered_list_block_component/numbered_list_character_shortcut.dart';
|
||||
|
||||
// quote
|
||||
export 'quote_block_component/quote_block_component.dart';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/block_component/base_component/markdown_format_helper.dart';
|
||||
|
||||
/// Convert '* ' to bulleted list
|
||||
///
|
||||
|
|
@ -38,45 +39,15 @@ Future<bool> _formatSymbolToBulletedList(
|
|||
) async {
|
||||
assert(symbol.length == 1);
|
||||
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
if (nodes.length != 1 || nodes.first.type == 'bulleted_list') {
|
||||
return false;
|
||||
}
|
||||
|
||||
final node = nodes.first;
|
||||
final delta = node.delta;
|
||||
if (delta == null) {
|
||||
return false;
|
||||
}
|
||||
final text = delta.toPlainText().substring(0, selection.end.offset);
|
||||
if (symbol != text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: node.path,
|
||||
offset: 0,
|
||||
return formatMarkdownSymbol(
|
||||
editorState,
|
||||
(node) => node.type != 'bulleted_list',
|
||||
(text, _) => text == symbol,
|
||||
(_, node, delta) => Node(
|
||||
type: 'bulleted_list',
|
||||
attributes: {
|
||||
'delta': delta.compose(Delta()..delete(symbol.length)).toJson(),
|
||||
},
|
||||
),
|
||||
);
|
||||
final bulletedListNode = Node(
|
||||
type: 'bulleted_list',
|
||||
attributes: {
|
||||
'delta': delta.compose(Delta()..delete(symbol.length)).toJson(),
|
||||
},
|
||||
);
|
||||
final transaction = editorState.transaction
|
||||
..insertNode(
|
||||
node.path,
|
||||
bulletedListNode,
|
||||
)
|
||||
..deleteNode(node)
|
||||
..afterSelection = afterSelection;
|
||||
await editorState.apply(transaction);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/block_component/base_component/markdown_format_helper.dart';
|
||||
|
||||
final _numberRegex = RegExp(r'^(\d+)\.');
|
||||
|
||||
/// Convert 'num. ' to bulleted list
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - mobile
|
||||
/// - web
|
||||
///
|
||||
CharacterShortcutEvent formatNumberToNumberedList = CharacterShortcutEvent(
|
||||
key: 'format number to numbered list',
|
||||
character: ' ',
|
||||
handler: (editorState) async => await formatMarkdownSymbol(
|
||||
editorState,
|
||||
(node) => node.type != 'numbered_list',
|
||||
(text, selection) {
|
||||
final match = _numberRegex.firstMatch(text);
|
||||
final matchText = match?.group(0);
|
||||
final numberText = match?.group(1);
|
||||
return match != null &&
|
||||
matchText != null &&
|
||||
numberText != null &&
|
||||
selection.endIndex == matchText.length;
|
||||
},
|
||||
(text, node, delta) {
|
||||
final match = _numberRegex.firstMatch(text)!;
|
||||
final matchText = match.group(0)!;
|
||||
return Node(
|
||||
type: 'numbered_list',
|
||||
attributes: {
|
||||
'delta': delta.compose(Delta()..delete(matchText.length)).toJson(),
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -28,6 +28,7 @@ dependencies:
|
|||
markdown: ^7.0.1
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
tuple: ^2.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../util/util.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
if (kDebugMode) {
|
||||
activateLog();
|
||||
}
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
if (kDebugMode) {
|
||||
deactivateLog();
|
||||
}
|
||||
});
|
||||
|
||||
group('formatNumberToNumberedList', () {
|
||||
// Before
|
||||
// 1|Welcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// 1|Welcome to AppFlowy Editor 🔥!
|
||||
test('mock inputting a ` ` after the number but not dot', () async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank().addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('1$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// 1|Welcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: 1),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final before = editorState.getNodesInSelection(selection).first;
|
||||
final result = await formatNumberToNumberedList.execute(editorState);
|
||||
final after = editorState.getNodesInSelection(selection).first;
|
||||
|
||||
// nothing happens
|
||||
expect(result, false);
|
||||
expect(before.toJson(), after.toJson());
|
||||
});
|
||||
|
||||
// Before
|
||||
// 1.|Welcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// [numbered_list]Welcome to AppFlowy Editor 🔥!
|
||||
test(
|
||||
'mock inputting a ` ` after the number which is located at the front of the text',
|
||||
() async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank().addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('1.$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// 1.|Welcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: 2),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final result = await formatNumberToNumberedList.execute(editorState);
|
||||
|
||||
expect(result, true);
|
||||
final after = editorState.getNodeAtPath([0])!;
|
||||
expect(after.delta!.toPlainText(), text);
|
||||
expect(after.type, 'numbered_list');
|
||||
});
|
||||
|
||||
// Before
|
||||
// 1.W|elcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// 1.W|elcome to AppFlowy Editor 🔥!
|
||||
test('mock inputting a ` ` in the middle of the node', () async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank().addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('1.$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// 1.W|elcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: 3),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final before = editorState.getNodesInSelection(selection).first;
|
||||
final result = await formatNumberToNumberedList.execute(editorState);
|
||||
final after = editorState.getNodesInSelection(selection).first;
|
||||
|
||||
// nothing happens
|
||||
expect(result, false);
|
||||
expect(before.toJson(), after.toJson());
|
||||
});
|
||||
|
||||
// Before
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
// 1.|Welcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
//[numbered_list] Welcome to AppFlowy Editor 🔥!
|
||||
test(
|
||||
'mock inputting a ` ` in the middle of the node, and there\'s a other node at the front of it.',
|
||||
() async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank()
|
||||
.addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert(text),
|
||||
)
|
||||
.addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('1.$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
// *|Welcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [1], offset: 2),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final result = await formatNumberToNumberedList.execute(editorState);
|
||||
final after = editorState.getNodeAtPath([1])!;
|
||||
|
||||
// the second line will be formatted as the bulleted list style
|
||||
expect(result, true);
|
||||
expect(after.type, 'numbered_list');
|
||||
expect(after.delta!.toPlainText(), text);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../util/util.dart';
|
||||
|
||||
void main() async {
|
||||
group('bulleted_list_shortcut.dart', () {
|
||||
setUpAll(() {
|
||||
if (kDebugMode) {
|
||||
activateLog();
|
||||
}
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
if (kDebugMode) {
|
||||
deactivateLog();
|
||||
}
|
||||
});
|
||||
|
||||
group('formatAsteriskToBulletedList', () {
|
||||
// Before
|
||||
// *|Welcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// [bulleted_list]Welcome to AppFlowy Editor 🔥!
|
||||
test(
|
||||
'mock inputting a ` ` after asterisk which is located at the front of the text',
|
||||
() async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank().addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('*$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// *|Welcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: 1),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final result = await formatAsteriskToBulletedList.execute(editorState);
|
||||
|
||||
expect(result, true);
|
||||
final after = editorState.getNodeAtPath([0])!;
|
||||
expect(after.delta!.toPlainText(), text);
|
||||
expect(after.type, 'bulleted_list');
|
||||
});
|
||||
|
||||
// Before
|
||||
// *W|elcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// *W|elcome to AppFlowy Editor 🔥!
|
||||
test('mock inputting a ` ` in the middle of the text', () async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank().addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('*$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// *W|elcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: 2),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final before = editorState.getNodesInSelection(selection).first;
|
||||
final result = await formatAsteriskToBulletedList.execute(editorState);
|
||||
final after = editorState.getNodesInSelection(selection).first;
|
||||
|
||||
// nothing happens
|
||||
expect(result, false);
|
||||
expect(before.toJson(), after.toJson());
|
||||
});
|
||||
|
||||
// Before
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
// *|Welcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
//[bulleted_list] Welcome to AppFlowy Editor 🔥!
|
||||
test('mock inputting a ` ` in the middle of the text', () async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank()
|
||||
.addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert(text),
|
||||
)
|
||||
.addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('*$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
// *|Welcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [1], offset: 1),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final result = await formatAsteriskToBulletedList.execute(editorState);
|
||||
final after = editorState.getNodeAtPath([1])!;
|
||||
|
||||
// the second line will be formatted as the bulleted list style
|
||||
expect(result, true);
|
||||
expect(after.type, 'bulleted_list');
|
||||
expect(after.delta!.toPlainText(), text);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../util/util.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
if (kDebugMode) {
|
||||
activateLog();
|
||||
}
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
if (kDebugMode) {
|
||||
deactivateLog();
|
||||
}
|
||||
});
|
||||
|
||||
group('formatAsteriskToBulletedList', () {
|
||||
// Before
|
||||
// *|Welcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// [bulleted_list]Welcome to AppFlowy Editor 🔥!
|
||||
test(
|
||||
'mock inputting a ` ` after asterisk which is located at the front of the text',
|
||||
() async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank().addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('*$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// *|Welcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: 1),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final result = await formatAsteriskToBulletedList.execute(editorState);
|
||||
|
||||
expect(result, true);
|
||||
final after = editorState.getNodeAtPath([0])!;
|
||||
expect(after.delta!.toPlainText(), text);
|
||||
expect(after.type, 'bulleted_list');
|
||||
});
|
||||
|
||||
// Before
|
||||
// *W|elcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// *W|elcome to AppFlowy Editor 🔥!
|
||||
test('mock inputting a ` ` in the middle of the text', () async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank().addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('*$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// *W|elcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: 2),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final before = editorState.getNodesInSelection(selection).first;
|
||||
final result = await formatAsteriskToBulletedList.execute(editorState);
|
||||
final after = editorState.getNodesInSelection(selection).first;
|
||||
|
||||
// nothing happens
|
||||
expect(result, false);
|
||||
expect(before.toJson(), after.toJson());
|
||||
});
|
||||
|
||||
// Before
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
// *|Welcome to AppFlowy Editor 🔥!
|
||||
// After
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
//[bulleted_list] Welcome to AppFlowy Editor 🔥!
|
||||
test('mock inputting a ` ` in the middle of the text', () async {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
final document = Document.blank()
|
||||
.addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert(text),
|
||||
)
|
||||
.addParagraphs(
|
||||
1,
|
||||
builder: (index) => Delta()..insert('*$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
// *|Welcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [1], offset: 1),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
final result = await formatAsteriskToBulletedList.execute(editorState);
|
||||
final after = editorState.getNodeAtPath([1])!;
|
||||
|
||||
// the second line will be formatted as the bulleted list style
|
||||
expect(result, true);
|
||||
expect(after.type, 'bulleted_list');
|
||||
expect(after.delta!.toPlainText(), text);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue