mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-10-31 18:15:09 +00:00 
			
		
		
		
	fix: support exporting more content to markdown (#7333)
* fix: support exporting to markdown with multiple images * fix: support exporting to markdown with database * fix: support exporting to markdown with date or reminder * fix: support exporting to markdown with subpage and page reference * chore: add some testing for markdown parser * chore: add testing for exporting markdown with databse as csv
This commit is contained in:
		
							parent
							
								
									04e3246976
								
							
						
					
					
						commit
						552dba5abe
					
				| @ -1,12 +1,16 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| 
 | ||||
| import 'package:appflowy/plugins/shared/share/share_button.dart'; | ||||
| import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; | ||||
| import 'package:archive/archive.dart'; | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| import 'package:integration_test/integration_test.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| 
 | ||||
| import '../../shared/mock/mock_file_picker.dart'; | ||||
| import '../../shared/util.dart'; | ||||
| import '../document/document_with_database_test.dart'; | ||||
| 
 | ||||
| void main() { | ||||
|   IntegrationTestWidgetsFlutterBinding.ensureInitialized(); | ||||
| @ -18,7 +22,7 @@ void main() { | ||||
| 
 | ||||
|       // mock the file picker | ||||
|       final path = await mockSaveFilePath( | ||||
|         p.join(context.applicationDataDirectory, 'test.md'), | ||||
|         p.join(context.applicationDataDirectory, 'test.zip'), | ||||
|       ); | ||||
|       // click the share button and select markdown | ||||
|       await tester.tapShareButton(); | ||||
| @ -28,10 +32,14 @@ void main() { | ||||
|       tester.expectToExportSuccess(); | ||||
| 
 | ||||
|       final file = File(path); | ||||
|       final isExist = file.existsSync(); | ||||
|       expect(isExist, true); | ||||
|       final markdown = file.readAsStringSync(); | ||||
|       expect(markdown, expectedMarkdown); | ||||
|       expect(file.existsSync(), true); | ||||
|       final archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); | ||||
|       for (final entry in archive) { | ||||
|         if (entry.isFile && entry.name.endsWith('.md')) { | ||||
|           final markdown = utf8.decode(entry.content); | ||||
|           expect(markdown, expectedMarkdown); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     testWidgets( | ||||
| @ -57,7 +65,7 @@ void main() { | ||||
|         final path = await mockSaveFilePath( | ||||
|           p.join( | ||||
|             context.applicationDataDirectory, | ||||
|             '${shareButtonState.view.name}.md', | ||||
|             '${shareButtonState.view.name}.zip', | ||||
|           ), | ||||
|         ); | ||||
| 
 | ||||
| @ -69,10 +77,44 @@ void main() { | ||||
|         tester.expectToExportSuccess(); | ||||
| 
 | ||||
|         final file = File(path); | ||||
|         final isExist = file.existsSync(); | ||||
|         expect(isExist, true); | ||||
|         expect(file.existsSync(), true); | ||||
|         final archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); | ||||
|         for (final entry in archive) { | ||||
|           if (entry.isFile && entry.name.endsWith('.md')) { | ||||
|             final markdown = utf8.decode(entry.content); | ||||
|             expect(markdown, expectedMarkdown); | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
| 
 | ||||
|     testWidgets('share the markdown with database', (tester) async { | ||||
|       final context = await tester.initializeAppFlowy(); | ||||
|       await tester.tapAnonymousSignInButton(); | ||||
|       await insertLinkedDatabase(tester, ViewLayoutPB.Grid); | ||||
| 
 | ||||
|       // mock the file picker | ||||
|       final path = await mockSaveFilePath( | ||||
|         p.join(context.applicationDataDirectory, 'test.zip'), | ||||
|       ); | ||||
|       // click the share button and select markdown | ||||
|       await tester.tapShareButton(); | ||||
|       await tester.tapMarkdownButton(); | ||||
| 
 | ||||
|       // expect to see the success dialog | ||||
|       tester.expectToExportSuccess(); | ||||
| 
 | ||||
|       final file = File(path); | ||||
|       expect(file.existsSync(), true); | ||||
|       final archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); | ||||
|       bool hasCsvFile = false; | ||||
|       for (final entry in archive) { | ||||
|         if (entry.isFile && entry.name.endsWith('.csv')) { | ||||
|           hasCsvFile = true; | ||||
|         } | ||||
|       } | ||||
|       expect(hasCsvFile, true); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -11,8 +11,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_bloc/flutter_bloc.dart'; | ||||
| 
 | ||||
| import 'widgets/ask_ai_action.dart'; | ||||
| import 'ask_ai_block_component.dart'; | ||||
| import 'widgets/ask_ai_action.dart'; | ||||
| 
 | ||||
| const _kAskAIToolbarItemId = 'appflowy.editor.ask_ai'; | ||||
| 
 | ||||
| @ -118,7 +118,7 @@ class _AskAIActionListState extends State<AskAIActionList> { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     final markdown = editorState.getMarkdownInSelection(selection); | ||||
|     final markdown = await editorState.getMarkdownInSelection(selection); | ||||
| 
 | ||||
|     final transaction = editorState.transaction; | ||||
|     transaction.insertNode( | ||||
|  | ||||
| @ -2,7 +2,7 @@ import 'package:appflowy/shared/markdown_to_document.dart'; | ||||
| import 'package:appflowy_editor/appflowy_editor.dart'; | ||||
| 
 | ||||
| extension AskAINodeExtension on EditorState { | ||||
|   String getMarkdownInSelection(Selection? selection) { | ||||
|   Future<String> getMarkdownInSelection(Selection? selection) async { | ||||
|     selection ??= this.selection?.normalized; | ||||
|     if (selection == null || selection.isCollapsed) { | ||||
|       return ''; | ||||
| @ -33,7 +33,7 @@ extension AskAINodeExtension on EditorState { | ||||
|       slicedNodes.add(copiedNode); | ||||
|     } | ||||
| 
 | ||||
|     final markdown = customDocumentToMarkdown( | ||||
|     final markdown = await customDocumentToMarkdown( | ||||
|       Document.blank()..insert([0], slicedNodes), | ||||
|     ); | ||||
| 
 | ||||
|  | ||||
| @ -13,10 +13,11 @@ import 'package:universal_platform/universal_platform.dart'; | ||||
| 
 | ||||
| const kMultiImagePlaceholderKey = 'multiImagePlaceholderKey'; | ||||
| 
 | ||||
| Node multiImageNode() => Node( | ||||
| Node multiImageNode({List<ImageBlockData>? images}) => Node( | ||||
|       type: MultiImageBlockKeys.type, | ||||
|       attributes: { | ||||
|         MultiImageBlockKeys.images: MultiImageData(images: []).toJson(), | ||||
|         MultiImageBlockKeys.images: | ||||
|             MultiImageData(images: images ?? []).toJson(), | ||||
|         MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(), | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
| @ -1,4 +1,9 @@ | ||||
| import 'dart:io'; | ||||
| 
 | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; | ||||
| import 'package:appflowy_editor/appflowy_editor.dart'; | ||||
| import 'package:archive/archive.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| 
 | ||||
| import '../image/custom_image_block_component/custom_image_block_component.dart'; | ||||
| 
 | ||||
| @ -16,3 +21,64 @@ class CustomImageNodeParser extends NodeParser { | ||||
|     return '\n'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class CustomImageNodeFileParser extends NodeParser { | ||||
|   const CustomImageNodeFileParser(this.files, this.dirPath); | ||||
| 
 | ||||
|   final List<Future<ArchiveFile>> files; | ||||
|   final String dirPath; | ||||
| 
 | ||||
|   @override | ||||
|   String get id => ImageBlockKeys.type; | ||||
| 
 | ||||
|   @override | ||||
|   String transform(Node node, DocumentMarkdownEncoder? encoder) { | ||||
|     assert(node.children.isEmpty); | ||||
|     final url = node.attributes[CustomImageBlockKeys.url]; | ||||
|     final hasFile = File(url).existsSync(); | ||||
|     if (hasFile) { | ||||
|       final bytes = File(url).readAsBytesSync(); | ||||
|       files.add( | ||||
|         Future.value( | ||||
|           ArchiveFile(p.join(dirPath, p.basename(url)), bytes.length, bytes), | ||||
|         ), | ||||
|       ); | ||||
|       return ')})\n'; | ||||
|     } | ||||
|     assert(url != null); | ||||
|     return '\n'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class CustomMultiImageNodeFileParser extends NodeParser { | ||||
|   const CustomMultiImageNodeFileParser(this.files, this.dirPath); | ||||
| 
 | ||||
|   final List<Future<ArchiveFile>> files; | ||||
|   final String dirPath; | ||||
| 
 | ||||
|   @override | ||||
|   String get id => MultiImageBlockKeys.type; | ||||
| 
 | ||||
|   @override | ||||
|   String transform(Node node, DocumentMarkdownEncoder? encoder) { | ||||
|     assert(node.children.isEmpty); | ||||
|     final images = node.attributes[MultiImageBlockKeys.images] as List; | ||||
|     final List<String> markdownImages = []; | ||||
|     for (final image in images) { | ||||
|       final String url = image['url'] ?? ''; | ||||
|       if (url.isEmpty) continue; | ||||
|       final hasFile = File(url).existsSync(); | ||||
|       if (hasFile) { | ||||
|         final bytes = File(url).readAsBytesSync(); | ||||
|         final filePath = p.join(dirPath, p.basename(url)); | ||||
|         files.add( | ||||
|           Future.value(ArchiveFile(filePath, bytes.length, bytes)), | ||||
|         ); | ||||
|         markdownImages.add(''); | ||||
|       } else { | ||||
|         markdownImages.add(''); | ||||
|       } | ||||
|     } | ||||
|     return markdownImages.join('\n'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,37 @@ | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; | ||||
| import 'package:appflowy_editor/appflowy_editor.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| 
 | ||||
| class CustomParagraphNodeParser extends NodeParser { | ||||
|   const CustomParagraphNodeParser(); | ||||
| 
 | ||||
|   @override | ||||
|   String get id => ParagraphBlockKeys.type; | ||||
| 
 | ||||
|   @override | ||||
|   String transform(Node node, DocumentMarkdownEncoder? encoder) { | ||||
|     final delta = node.delta; | ||||
|     if (delta != null) { | ||||
|       for (final o in delta) { | ||||
|         final attribute = o.attributes ?? {}; | ||||
|         final Map? mention = attribute[MentionBlockKeys.mention] ?? {}; | ||||
|         if (mention == null) continue; | ||||
| 
 | ||||
|         /// filter date reminder node, and return it | ||||
|         final String date = mention[MentionBlockKeys.date] ?? ''; | ||||
|         if (date.isNotEmpty) { | ||||
|           final dateTime = DateTime.tryParse(date); | ||||
|           if (dateTime == null) continue; | ||||
|           return '${DateFormat.yMMMd().format(dateTime)}\n'; | ||||
|         } | ||||
| 
 | ||||
|         /// filter reference page | ||||
|         final String pageId = mention[MentionBlockKeys.pageId] ?? ''; | ||||
|         if (pageId.isNotEmpty) { | ||||
|           return '[]($pageId)\n'; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return const TextNodeParser().transform(node, encoder); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,53 @@ | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart'; | ||||
| import 'package:appflowy/workspace/application/settings/share/export_service.dart'; | ||||
| import 'package:appflowy_backend/log.dart'; | ||||
| import 'package:appflowy_editor/appflowy_editor.dart'; | ||||
| import 'package:archive/archive.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| 
 | ||||
| abstract class DatabaseNodeParser extends NodeParser { | ||||
|   DatabaseNodeParser(this.files, this.dirPath); | ||||
| 
 | ||||
|   final List<Future<ArchiveFile>> files; | ||||
|   final String dirPath; | ||||
| 
 | ||||
|   @override | ||||
|   String transform(Node node, DocumentMarkdownEncoder? encoder) { | ||||
|     final String viewId = node.attributes[DatabaseBlockKeys.viewID] ?? ''; | ||||
|     if (viewId.isEmpty) return ''; | ||||
|     files.add(_convertDatabaseToCSV(viewId)); | ||||
|     return '[](${p.join(dirPath, '$viewId.csv')})\n'; | ||||
|   } | ||||
| 
 | ||||
|   Future<ArchiveFile> _convertDatabaseToCSV(String viewId) async { | ||||
|     final result = await BackendExportService.exportDatabaseAsCSV(viewId); | ||||
|     final filePath = p.join(dirPath, '$viewId.csv'); | ||||
|     ArchiveFile file = ArchiveFile.string(filePath, ''); | ||||
|     result.fold( | ||||
|       (s) => file = ArchiveFile.string(filePath, s.data), | ||||
|       (f) => Log.error('convertDatabaseToCSV error with $viewId, error: $f'), | ||||
|     ); | ||||
|     return file; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class GridNodeParser extends DatabaseNodeParser { | ||||
|   GridNodeParser(super.files, super.dirPath); | ||||
| 
 | ||||
|   @override | ||||
|   String get id => DatabaseBlockKeys.gridType; | ||||
| } | ||||
| 
 | ||||
| class BoardNodeParser extends DatabaseNodeParser { | ||||
|   BoardNodeParser(super.files, super.dirPath); | ||||
| 
 | ||||
|   @override | ||||
|   String get id => DatabaseBlockKeys.boardType; | ||||
| } | ||||
| 
 | ||||
| class CalendarNodeParser extends DatabaseNodeParser { | ||||
|   CalendarNodeParser(super.files, super.dirPath); | ||||
| 
 | ||||
|   @override | ||||
|   String get id => DatabaseBlockKeys.calendarType; | ||||
| } | ||||
| @ -1,5 +1,7 @@ | ||||
| export 'callout_node_parser.dart'; | ||||
| export 'custom_image_node_parser.dart'; | ||||
| export 'custom_paragraph_node_parser.dart'; | ||||
| export 'database_node_parser.dart'; | ||||
| export 'file_block_node_parser.dart'; | ||||
| export 'link_preview_node_parser.dart'; | ||||
| export 'math_equation_node_parser.dart'; | ||||
|  | ||||
| @ -0,0 +1,20 @@ | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; | ||||
| import 'package:appflowy_editor/appflowy_editor.dart'; | ||||
| 
 | ||||
| class SubPageNodeParser extends NodeParser { | ||||
|   const SubPageNodeParser(); | ||||
| 
 | ||||
|   @override | ||||
|   String get id => SubPageBlockKeys.type; | ||||
| 
 | ||||
|   @override | ||||
|   String transform(Node node, DocumentMarkdownEncoder? encoder) { | ||||
|     final String viewId = node.attributes[SubPageBlockKeys.viewId] ?? ''; | ||||
|     if (viewId.isNotEmpty) { | ||||
|       final view = pageMemorizer[viewId]; | ||||
|       return '[$viewId](${view?.name ?? ''})\n'; | ||||
|     } | ||||
|     return ''; | ||||
|   } | ||||
| } | ||||
| @ -105,7 +105,7 @@ class ExportTab extends StatelessWidget { | ||||
|     final viewName = context.read<ShareBloc>().state.viewName; | ||||
|     final exportPath = await getIt<FilePickerService>().saveFile( | ||||
|       dialogTitle: '', | ||||
|       fileName: '${viewName.toFileName()}.md', | ||||
|       fileName: '${viewName.toFileName()}.zip', | ||||
|     ); | ||||
|     if (context.mounted && exportPath != null) { | ||||
|       context.read<ShareBloc>().add( | ||||
|  | ||||
| @ -324,19 +324,21 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> { | ||||
|         (f) => FlowyResult.failure(f), | ||||
|       ); | ||||
|     } else { | ||||
|       result = await documentExporter.export(type.documentExportType); | ||||
|       result = | ||||
|           await documentExporter.export(type.documentExportType, path: path); | ||||
|     } | ||||
|     return result.fold( | ||||
|       (s) { | ||||
|         if (path != null) { | ||||
|           switch (type) { | ||||
|             case ShareType.markdown: | ||||
|             case ShareType.html: | ||||
|             case ShareType.csv: | ||||
|             case ShareType.json: | ||||
|             case ShareType.rawDatabaseData: | ||||
|               File(path).writeAsStringSync(s); | ||||
|               return FlowyResult.success(type); | ||||
|             case ShareType.markdown: | ||||
|               return FlowyResult.success(type); | ||||
|             default: | ||||
|               break; | ||||
|           } | ||||
| @ -387,22 +389,30 @@ enum ShareType { | ||||
| @freezed | ||||
| class ShareEvent with _$ShareEvent { | ||||
|   const factory ShareEvent.initial() = _Initial; | ||||
| 
 | ||||
|   const factory ShareEvent.share( | ||||
|     ShareType type, | ||||
|     String? path, | ||||
|   ) = _Share; | ||||
| 
 | ||||
|   const factory ShareEvent.publish( | ||||
|     String nameSpace, | ||||
|     String pageId, | ||||
|     List<String> selectedViewIds, | ||||
|   ) = _Publish; | ||||
| 
 | ||||
|   const factory ShareEvent.unPublish() = _UnPublish; | ||||
| 
 | ||||
|   const factory ShareEvent.updateViewName(String name, String viewId) = | ||||
|       _UpdateViewName; | ||||
| 
 | ||||
|   const factory ShareEvent.updatePublishStatus() = _UpdatePublishStatus; | ||||
| 
 | ||||
|   const factory ShareEvent.setPublishStatus(bool isPublished) = | ||||
|       _SetPublishStatus; | ||||
| 
 | ||||
|   const factory ShareEvent.updatePathName(String pathName) = _UpdatePathName; | ||||
| 
 | ||||
|   const factory ShareEvent.clearPathNameResult() = _ClearPathNameResult; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,13 @@ | ||||
| import 'dart:io'; | ||||
| 
 | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart'; | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; | ||||
| import 'package:appflowy_backend/log.dart'; | ||||
| import 'package:appflowy_editor/appflowy_editor.dart'; | ||||
| import 'package:archive/archive.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| import 'package:share_plus/share_plus.dart'; | ||||
| 
 | ||||
| Document customMarkdownToDocument( | ||||
|   String markdown, { | ||||
| @ -14,17 +22,66 @@ Document customMarkdownToDocument( | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| String customDocumentToMarkdown(Document document) { | ||||
|   return documentToMarkdown( | ||||
| Future<String> customDocumentToMarkdown( | ||||
|   Document document, { | ||||
|   String path = '', | ||||
|   AsyncValueSetter<Archive>? onArchive, | ||||
| }) async { | ||||
|   final List<Future<ArchiveFile>> fileFutures = []; | ||||
| 
 | ||||
|   /// create root Archive and directory | ||||
|   final id = document.root.id, | ||||
|       archive = Archive(), | ||||
|       resourceDir = ArchiveFile('$id/', 0, null)..isFile = false, | ||||
|       fileName = p.basenameWithoutExtension(path), | ||||
|       dirName = resourceDir.name; | ||||
| 
 | ||||
|   final markdown = documentToMarkdown( | ||||
|     document, | ||||
|     customParsers: [ | ||||
|       const MathEquationNodeParser(), | ||||
|       const CalloutNodeParser(), | ||||
|       const ToggleListNodeParser(), | ||||
|       const CustomImageNodeParser(), | ||||
|       CustomImageNodeFileParser(fileFutures, dirName), | ||||
|       CustomMultiImageNodeFileParser(fileFutures, dirName), | ||||
|       GridNodeParser(fileFutures, dirName), | ||||
|       BoardNodeParser(fileFutures, dirName), | ||||
|       CalendarNodeParser(fileFutures, dirName), | ||||
|       const CustomParagraphNodeParser(), | ||||
|       const SubPageNodeParser(), | ||||
|       const SimpleTableNodeParser(), | ||||
|       const LinkPreviewNodeParser(), | ||||
|       const FileBlockNodeParser(), | ||||
|     ], | ||||
|   ); | ||||
| 
 | ||||
|   /// create resource directory | ||||
|   if (fileFutures.isNotEmpty) archive.addFile(resourceDir); | ||||
| 
 | ||||
|   /// add markdown file to Archive | ||||
|   archive.addFile(ArchiveFile.string('$fileName-$id.md', markdown)); | ||||
| 
 | ||||
|   for (final fileFuture in fileFutures) { | ||||
|     archive.addFile(await fileFuture); | ||||
|   } | ||||
|   if (archive.isNotEmpty && path.isNotEmpty) { | ||||
|     if (onArchive == null) { | ||||
|       final zipEncoder = ZipEncoder(); | ||||
|       final zip = zipEncoder.encode(archive); | ||||
|       if (zip != null) { | ||||
|         final zipFile = await File(path).writeAsBytes(zip); | ||||
|         if (Platform.isIOS) { | ||||
|           await Share.shareUri(zipFile.uri); | ||||
|           await zipFile.delete(); | ||||
|         } else if (Platform.isAndroid) { | ||||
|           await Share.shareXFiles([XFile(zipFile.path)]); | ||||
|           await zipFile.delete(); | ||||
|         } | ||||
|         Log.info('documentToMarkdownFiles to $path'); | ||||
|       } | ||||
|     } else { | ||||
|       await onArchive.call(archive); | ||||
|     } | ||||
|   } | ||||
|   return markdown; | ||||
| } | ||||
|  | ||||
| @ -25,12 +25,13 @@ class DocumentExporter { | ||||
|   final ViewPB view; | ||||
| 
 | ||||
|   Future<FlowyResult<String, FlowyError>> export( | ||||
|     DocumentExportType type, | ||||
|   ) async { | ||||
|     DocumentExportType type, { | ||||
|     String? path, | ||||
|   }) async { | ||||
|     final documentService = DocumentService(); | ||||
|     final result = await documentService.openDocument(documentId: view.id); | ||||
|     return result.fold( | ||||
|       (r) { | ||||
|       (r) async { | ||||
|         final document = r.toDocument(); | ||||
|         if (document == null) { | ||||
|           return FlowyResult.failure( | ||||
| @ -43,8 +44,14 @@ class DocumentExporter { | ||||
|           case DocumentExportType.json: | ||||
|             return FlowyResult.success(jsonEncode(document)); | ||||
|           case DocumentExportType.markdown: | ||||
|             final markdown = customDocumentToMarkdown(document); | ||||
|             return FlowyResult.success(markdown); | ||||
|             if (path != null) { | ||||
|               await customDocumentToMarkdown(document, path: path); | ||||
|               return FlowyResult.success(''); | ||||
|             } else { | ||||
|               return FlowyResult.success( | ||||
|                 await customDocumentToMarkdown(document), | ||||
|               ); | ||||
|             } | ||||
|           case DocumentExportType.text: | ||||
|             throw UnimplementedError(); | ||||
|           case DocumentExportType.html: | ||||
|  | ||||
| @ -1,7 +1,10 @@ | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; | ||||
| import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; | ||||
| import 'package:appflowy/shared/markdown_to_document.dart'; | ||||
| import 'package:appflowy_editor/appflowy_editor.dart'; | ||||
| import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| 
 | ||||
| void main() { | ||||
| @ -17,21 +20,78 @@ void main() { | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       final markdown = customDocumentToMarkdown(document); | ||||
|       final markdown = await customDocumentToMarkdown(document); | ||||
|       expect(markdown, '[file.txt](https://file.com)\n'); | ||||
|     }); | ||||
| 
 | ||||
|     test('link preview', () { | ||||
|     test('link preview', () async { | ||||
|       final document = Document.blank() | ||||
|         ..insert( | ||||
|           [0], | ||||
|           [linkPreviewNode(url: 'https://www.link_preview.com')], | ||||
|         ); | ||||
|       final markdown = customDocumentToMarkdown(document); | ||||
|       final markdown = await customDocumentToMarkdown(document); | ||||
|       expect( | ||||
|         markdown, | ||||
|         '[https://www.link_preview.com](https://www.link_preview.com)\n', | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     test('multiple images', () async { | ||||
|       const png1 = 'https://www.appflowy.png', | ||||
|           png2 = 'https://www.appflowy2.png'; | ||||
|       final document = Document.blank() | ||||
|         ..insert( | ||||
|           [0], | ||||
|           [ | ||||
|             multiImageNode( | ||||
|               images: [ | ||||
|                 ImageBlockData( | ||||
|                   url: png1, | ||||
|                   type: CustomImageType.external, | ||||
|                 ), | ||||
|                 ImageBlockData( | ||||
|                   url: png2, | ||||
|                   type: CustomImageType.external, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       final markdown = await customDocumentToMarkdown(document); | ||||
|       expect( | ||||
|         markdown, | ||||
|         '\n', | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     test('subpage block', () async { | ||||
|       const testSubpageId = 'testSubpageId'; | ||||
|       final subpageNode = pageMentionNode(testSubpageId); | ||||
|       final document = Document.blank() | ||||
|         ..insert( | ||||
|           [0], | ||||
|           [subpageNode], | ||||
|         ); | ||||
|       final markdown = await customDocumentToMarkdown(document); | ||||
|       expect( | ||||
|         markdown, | ||||
|         '[]($testSubpageId)\n', | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     test('date or reminder', () async { | ||||
|       final dateTime = DateTime.now(); | ||||
|       final document = Document.blank() | ||||
|         ..insert( | ||||
|           [0], | ||||
|           [dateMentionNode()], | ||||
|         ); | ||||
|       final markdown = await customDocumentToMarkdown(document); | ||||
|       expect( | ||||
|         markdown, | ||||
|         '${DateFormat.yMMMd().format(dateTime)}\n', | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Morn
						Morn