mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-17 10:14:47 +00:00
chore: support pasting image links that have no file extension (#7262)
* test: speed up copypasta tests * chore: strip away markdown syntax if message is image only * chore: paste image urls with no file extension * test: add integration test * test: group tests * chore: apply code suggestions to 3 files
This commit is contained in:
parent
0b0e10baa8
commit
c5a91e10df
@ -19,7 +19,7 @@ import '../../shared/util.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
group('copy and paste in document', () {
|
group('copy and paste in document:', () {
|
||||||
testWidgets('paste multiple lines at the first line', (tester) async {
|
testWidgets('paste multiple lines at the first line', (tester) async {
|
||||||
// mock the clipboard
|
// mock the clipboard
|
||||||
const lines = 3;
|
const lines = 3;
|
||||||
@ -172,7 +172,6 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('paste text on part of bullet list', (tester) async {
|
testWidgets('paste text on part of bullet list', (tester) async {
|
||||||
const plainText = 'test';
|
const plainText = 'test';
|
||||||
@ -470,6 +469,16 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('paste image url without extension', (tester) async {
|
||||||
|
const plainText =
|
||||||
|
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
|
||||||
|
await tester.pasteContent(plainText: plainText, (editorState) {
|
||||||
|
final node = editorState.getNodeAtPath([0])!;
|
||||||
|
expect(node.type, ImageBlockKeys.type);
|
||||||
|
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const testMarkdownText = '''
|
const testMarkdownText = '''
|
||||||
# I'm h1
|
# I'm h1
|
||||||
## I'm h2
|
## I'm h2
|
||||||
@ -507,6 +516,7 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on WidgetTester {
|
extension on WidgetTester {
|
||||||
@ -523,7 +533,7 @@ extension on WidgetTester {
|
|||||||
await tapAnonymousSignInButton();
|
await tapAnonymousSignInButton();
|
||||||
|
|
||||||
// create a new document
|
// create a new document
|
||||||
await createNewPageWithNameUnderParent(name: 'Test Document');
|
await createNewPageWithNameUnderParent();
|
||||||
// tap the editor
|
// tap the editor
|
||||||
await tapButton(find.byType(AppFlowyEditor));
|
await tapButton(find.byType(AppFlowyEditor));
|
||||||
|
|
||||||
@ -546,7 +556,7 @@ extension on WidgetTester {
|
|||||||
isShiftPressed: pasteAsPlain,
|
isShiftPressed: pasteAsPlain,
|
||||||
isMetaPressed: Platform.isMacOS,
|
isMetaPressed: Platform.isMacOS,
|
||||||
);
|
);
|
||||||
await pumpAndSettle();
|
await pumpAndSettle(const Duration(milliseconds: 1000));
|
||||||
|
|
||||||
test(editor.getCurrentEditorState());
|
test(editor.getCurrentEditorState());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.d
|
|||||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
import 'package:appflowy/shared/markdown_to_document.dart';
|
import 'package:appflowy/shared/markdown_to_document.dart';
|
||||||
|
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/util/theme_extension.dart';
|
import 'package:appflowy/util/theme_extension.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||||
@ -170,7 +171,7 @@ class CopyButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
await getIt<ClipboardService>().setData(
|
await getIt<ClipboardService>().setData(
|
||||||
ClipboardServiceData(
|
ClipboardServiceData(
|
||||||
plainText: textMessage.text,
|
plainText: _getTrimmedPlainText(textMessage.text),
|
||||||
inAppJson: jsonEncode(document.toJson()),
|
inAppJson: jsonEncode(document.toJson()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -184,6 +185,16 @@ class CopyButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getTrimmedPlainText(String plainText) {
|
||||||
|
// match and capture inner url as group
|
||||||
|
final matches = singleLineMarkdownImageRegex.allMatches(plainText);
|
||||||
|
if (matches.length != 1) {
|
||||||
|
return plainText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches.first[1] ?? plainText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RegenerateButton extends StatelessWidget {
|
class RegenerateButton extends StatelessWidget {
|
||||||
|
|||||||
@ -8,12 +8,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart';
|
||||||
import 'package:appflowy/shared/clipboard_state.dart';
|
import 'package:appflowy/shared/clipboard_state.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/util/default_extensions.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:string_validator/string_validator.dart';
|
import 'package:string_validator/string_validator.dart';
|
||||||
|
|
||||||
/// - support
|
/// - support
|
||||||
@ -154,11 +154,8 @@ Future<bool> _pasteAsLinkPreview(
|
|||||||
EditorState editorState,
|
EditorState editorState,
|
||||||
String? text,
|
String? text,
|
||||||
) async {
|
) async {
|
||||||
// 1. the url should contains a protocol
|
// the url should contain a protocol
|
||||||
// 2. the url should not be an image url
|
if (text == null || !isURL(text, {'require_protocol': true})) {
|
||||||
if (text == null ||
|
|
||||||
text.isImageUrl() ||
|
|
||||||
!isURL(text, {'require_protocol': true})) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,10 +177,17 @@ Future<bool> _pasteAsLinkPreview(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. insert the text with link format
|
final bool isImageUrl;
|
||||||
// 2. convert it the link preview node
|
try {
|
||||||
final textTransaction = editorState.transaction;
|
isImageUrl = await _isImageUrl(text);
|
||||||
textTransaction.insertText(
|
} catch (e) {
|
||||||
|
Log.info('unable to get content header');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert the text with link format
|
||||||
|
final textTransaction = editorState.transaction
|
||||||
|
..insertText(
|
||||||
node,
|
node,
|
||||||
0,
|
0,
|
||||||
text,
|
text,
|
||||||
@ -194,23 +198,24 @@ Future<bool> _pasteAsLinkPreview(
|
|||||||
skipHistoryDebounce: true,
|
skipHistoryDebounce: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
final linkPreviewTransaction = editorState.transaction;
|
// convert it to image or link preview node
|
||||||
final insertedNodes = [
|
final replacementInsertedNodes = [
|
||||||
linkPreviewNode(url: text),
|
isImageUrl ? imageNode(url: text) : linkPreviewNode(url: text),
|
||||||
// if the next node is null, insert a empty paragraph node
|
// if the next node is null, insert a empty paragraph node
|
||||||
if (node.next == null) paragraphNode(),
|
if (node.next == null) paragraphNode(),
|
||||||
];
|
];
|
||||||
linkPreviewTransaction.insertNodes(
|
|
||||||
|
final replacementTransaction = editorState.transaction
|
||||||
|
..insertNodes(
|
||||||
selection.start.path,
|
selection.start.path,
|
||||||
insertedNodes,
|
replacementInsertedNodes,
|
||||||
|
)
|
||||||
|
..deleteNode(node)
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(path: node.path.next),
|
||||||
);
|
);
|
||||||
linkPreviewTransaction.deleteNode(node);
|
|
||||||
linkPreviewTransaction.afterSelection = Selection.collapsed(
|
await editorState.apply(replacementTransaction);
|
||||||
Position(
|
|
||||||
path: node.path.next,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await editorState.apply(linkPreviewTransaction);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -235,3 +240,16 @@ Future<void> doPlainPaste(EditorState editorState) async {
|
|||||||
Log.info('unable to parse the clipboard content');
|
Log.info('unable to parse the clipboard content');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _isImageUrl(String text) async {
|
||||||
|
final response = await http.head(Uri.parse(text));
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final contentType = response.headers['content-type'];
|
||||||
|
if (contentType != null) {
|
||||||
|
return contentType.startsWith('image/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'bad status code';
|
||||||
|
}
|
||||||
|
|||||||
@ -13,6 +13,9 @@ const _imgUrlPattern =
|
|||||||
r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.jpeg|.gif|.webm|.webp|.bmp)(\?[^\s[",><]*)?';
|
r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.jpeg|.gif|.webm|.webp|.bmp)(\?[^\s[",><]*)?';
|
||||||
final imgUrlRegex = RegExp(_imgUrlPattern);
|
final imgUrlRegex = RegExp(_imgUrlPattern);
|
||||||
|
|
||||||
|
const _singleLineMarkdownImagePattern = "^!\\[.*\\]\\(($_hrefPattern)\\)\$";
|
||||||
|
final singleLineMarkdownImageRegex = RegExp(_singleLineMarkdownImagePattern);
|
||||||
|
|
||||||
/// This pattern allows for both HTTP and HTTPS Scheme
|
/// This pattern allows for both HTTP and HTTPS Scheme
|
||||||
/// It allows for query parameters
|
/// It allows for query parameters
|
||||||
/// It only allows the following video extensions:
|
/// It only allows the following video extensions:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user