mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-25 15:17:28 -04: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
14 changed files with 383 additions and 28 deletions
|
@ -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…
Add table
Add a link
Reference in a new issue