diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart index 9a4fe30815..8c3c29ab77 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart @@ -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); + }); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart index 723b7f4ffb..9b413e1270 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart @@ -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 { return; } - final markdown = editorState.getMarkdownInSelection(selection); + final markdown = await editorState.getMarkdownInSelection(selection); final transaction = editorState.transaction; transaction.insertNode( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart index 933712217f..9ed5b046b9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart @@ -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 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), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart index 272b492835..88f60db494 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart @@ -13,10 +13,11 @@ import 'package:universal_platform/universal_platform.dart'; const kMultiImagePlaceholderKey = 'multiImagePlaceholderKey'; -Node multiImageNode() => Node( +Node multiImageNode({List? images}) => Node( type: MultiImageBlockKeys.type, attributes: { - MultiImageBlockKeys.images: MultiImageData(images: []).toJson(), + MultiImageBlockKeys.images: + MultiImageData(images: images ?? []).toJson(), MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(), }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart index 91398302ed..d4b6bb444f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart @@ -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 '![]($url)\n'; } } + +class CustomImageNodeFileParser extends NodeParser { + const CustomImageNodeFileParser(this.files, this.dirPath); + + final List> 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 '![](${p.join(dirPath, p.basename(url))})\n'; + } + assert(url != null); + return '![]($url)\n'; + } +} + +class CustomMultiImageNodeFileParser extends NodeParser { + const CustomMultiImageNodeFileParser(this.files, this.dirPath); + + final List> 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 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('![]($filePath)'); + } else { + markdownImages.add('![]($url)'); + } + } + return markdownImages.join('\n'); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart new file mode 100644 index 0000000000..b7d7674137 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart @@ -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); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart new file mode 100644 index 0000000000..3ba599d491 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart @@ -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> 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 _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; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart index 3f2895d57e..c0a15629b8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart new file mode 100644 index 0000000000..1cf0c569bc --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_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 ''; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index bf0b7fee26..f788d99eb7 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -105,7 +105,7 @@ class ExportTab extends StatelessWidget { final viewName = context.read().state.viewName; final exportPath = await getIt().saveFile( dialogTitle: '', - fileName: '${viewName.toFileName()}.md', + fileName: '${viewName.toFileName()}.zip', ); if (context.mounted && exportPath != null) { context.read().add( diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index c42bbda5a0..a852fa5e38 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -324,19 +324,21 @@ class ShareBloc extends Bloc { (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 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; } diff --git a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart index 5763906b57..5664211828 100644 --- a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart +++ b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart @@ -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 customDocumentToMarkdown( + Document document, { + String path = '', + AsyncValueSetter? onArchive, +}) async { + final List> 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; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart index df46ab97e7..a17b5741bc 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart @@ -25,12 +25,13 @@ class DocumentExporter { final ViewPB view; Future> 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: diff --git a/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart b/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart index 70775612e2..707cc23d4f 100644 --- a/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart @@ -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, + '![]($png1)\n![]($png2)', + ); + }); + + 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', + ); + }); }); }