Skip to content

Commit

Permalink
feat: added support for importing subpages along with the page while …
Browse files Browse the repository at this point in the history
…importing from notion
  • Loading branch information
Mukund-Tandon committed Aug 30, 2023
1 parent 2466cfc commit 94dc5b9
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:path/path.dart' as p;

import 'importer/custom_parsers/subpage_import_parser.dart';
import 'package:markdown/markdown.dart';

typedef ImportCallback = void Function(
ImportType? type,
ImportFromNotionType? notionType,
Expand Down Expand Up @@ -114,10 +117,10 @@ class ImportPanel extends StatelessWidget {
parentViewId: parentViewId,
);
await notionImporter.importFromNotion(type, path);
importCallback(null, ImportFromNotionType.markdownZip, '', null);
if (context.mounted) {
FlowyOverlay.pop(context);
}
importCallback(null, ImportFromNotionType.markdownZip, '', null);
}
},
),
Expand Down Expand Up @@ -193,7 +196,11 @@ class ImportPanel extends StatelessWidget {
Uint8List? documentDataFrom(ImportType importType, String data) {
switch (importType) {
case ImportType.markdownOrText:
final document = markdownToDocument(data);
final List<InlineSyntax> inlineSyntaxes = [
SubPageInlineSyntax(),
];
final document =
markdownToDocument(data, customInlineSyntaxes: inlineSyntaxes);
return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
case ImportType.historyDocument:
final document = EditorMigration.migrateDocument(data);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:markdown/markdown.dart' as md;

import '../../../../../../../../plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';

class SubPageInlineSyntax extends md.InlineSyntax {
SubPageInlineSyntax() : super(r'{{AppFlowy-Subpage}}\{(.*?)\}\{(.*?)\}');

@override
bool onMatch(md.InlineParser parser, Match match) {
final md.Element el = md.Element.text('mention_block', "\$");
el.attributes[MentionBlockKeys.mention] = '''{
"${MentionBlockKeys.type}": "${MentionType.page.name}",
"${MentionBlockKeys.pageId}": "${match.group(2)}"
}''';
parser.addNode(el);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
import 'dart:convert';
import 'dart:io';

import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart';

import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:archive/archive_io.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;

import '../import_panel.dart';

//PageToImport class has the details of the page to be imported
class PageToImport {
PageToImport({
required this.page,
required this.parentName,
});
ArchiveFile page;
String parentName;
}

//Level class hass all the details about a level of the imported pages
class Level {
Level({
required this.assetsAtThelevel,
required this.pagesAtTheLevel,
});
List<ArchiveFile> assetsAtThelevel;
List<PageToImport> pagesAtTheLevel;
}

class NotionImporter {
NotionImporter({
required this.parentViewId,
});

final String parentViewId;

final markdownImageRegex = RegExp(r'^!\[[^\]]*\]\((.*?)\)');
final fileRegex = RegExp(r'\[([^\]]*)\]\(([^\)]*)\)');

Future<void> importFromNotion(ImportFromNotionType type, String path) async {
switch (type) {
Expand All @@ -38,6 +55,10 @@ class NotionImporter {
final zip = File(path);
final bytes = await zip.readAsBytes();
final unzipFiles = ZipDecoder().decodeBytes(bytes);
final List<Level> levels = []; //list of all the levels of pages
final Map<String, String> nameToId =
{}; // this map store page name and viewID
//first we are get the main page and all assets of the main page
ArchiveFile? mainpage;
final List<ArchiveFile> mainpageAssets = [];
for (final element in unzipFiles) {
Expand Down Expand Up @@ -68,76 +89,43 @@ class NotionImporter {
unzipFiles.files.remove(element);
}

//first we are importing the main page
final mainPageName = p.basenameWithoutExtension(mainpage.name);
final markdownContents = utf8.decode(mainpage.content as Uint8List);
final processedMarkdownFile = await _preProcessMarkdownFile(
markdownContents,
mainpageAssets,
);
final data = documentDataFrom(
ImportType.markdownOrText,
processedMarkdownFile,
);
final result = await ViewBackendService.createView(
layoutType: ViewLayoutPB.Document,
name: mainPageName,
parentViewId: parentViewId,
initialDataBytes: data,
);
//this map will store the name of the view as key and its viewID are value
final Map<String, String> parentNameToId = {};
String mainPageId;
if (result.isLeft()) {
mainPageId = result.getLeftOrNull()!.id;
} else {
return;
}
parentNameToId[mainPageName] = mainPageId;
// now we will import the sub pages
// Each iteration of the below while loops will be importing one level of
// pages, like the main page and its assets would be consider level one then
// if main page contains any subpages then those sub pages along with the
// assets in those subpages will be considered level 2 and if any of those
// subpages contains any subpage that would be level 3 and so on. But we
// have already imported the main page this while loop wil start from level
// 2
// now we store each level of pages inside levels list
// The below while loop will iterate through all the unzipfiles and stores
// them in levels list according to the level at which the file belong and
// with the assets at that level.This mainly deals with the subpages of the
// main page for example the main page can form level one and if it has a
// subpage it can be level 2 and if that sub page also have a subpage then
// that will belong to level 3
while (unzipFiles.isNotEmpty) {
final List<ArchiveFile> files = [];
final List<PageToImport> files = [];
final List<ArchiveFile> images = [];
final List<ArchiveFile> folders = [];
// This for loop gets all the markdown files at a level
for (int i = 0; i < unzipFiles.length; i++) {
if (unzipFiles[i].isFile &&
['.png', '.jpg', '.jpeg']
.contains(p.extension(unzipFiles[i].name)) &&
unzipFiles[i].name.endsWith('.md') &&
unzipFiles[i].name.split('/').length - 1 == 1) {
images.add(unzipFiles[i]);
} else if (unzipFiles[i].isFile &&
['.png', '.jpg', '.jpeg']
.contains(p.extension(unzipFiles[i].name))) {
final String parentName = unzipFiles[i].name.split('/')[0];
files.add(PageToImport(page: unzipFiles[i], parentName: parentName));
} else if (unzipFiles[i].isFile && unzipFiles[i].name.endsWith('.md')) {
final List<String> segments = unzipFiles[i].name.split('/');
segments.removeAt(0);
unzipFiles[i].name = segments.join('/');
}
}
if (files.isEmpty) {
return;
}
// This for loop gets all the assets at a level
for (int i = 0; i < unzipFiles.length; i++) {
if (unzipFiles[i].isFile &&
unzipFiles[i].name.endsWith('.md') &&
['.png', '.jpg', '.jpeg']
.contains(p.extension(unzipFiles[i].name)) &&
unzipFiles[i].name.split('/').length - 1 == 1) {
final String parentName = unzipFiles[i].name.split('/')[0];
final String? parentViewId = parentNameToId[parentName];
if (parentViewId == null) {
return;
}
final createdpageId =
await _createPage(parentViewId, unzipFiles[i], images);
if (createdpageId == null) {
return;
}
final name = p.basenameWithoutExtension(unzipFiles[i].name);
parentNameToId[name] = createdpageId;
files.add(unzipFiles[i]);
} else if (unzipFiles[i].isFile && unzipFiles[i].name.endsWith('.md')) {
images.add(unzipFiles[i]);
} else if (unzipFiles[i].isFile &&
['.png', '.jpg', '.jpeg']
.contains(p.extension(unzipFiles[i].name))) {
final List<String> segments = unzipFiles[i].name.split('/');
segments.removeAt(0);
unzipFiles[i].name = segments.join('/');
Expand All @@ -147,31 +135,83 @@ class NotionImporter {
folders.add(unzipFiles[i]);
}
}
if (files.isEmpty) {
return;
levels.add(Level(assetsAtThelevel: images, pagesAtTheLevel: files));
// removing all the files that are already added in the levels list
for (final element in files) {
unzipFiles.files.remove(element.page);
}
for (final element in folders) {
unzipFiles.files.remove(element);
}
for (final element in files) {
unzipFiles.files.remove(element);
}
for (final element in images) {
unzipFiles.files.remove(element);
}
}
final int noOfLevels = levels.length;
// This for loop will iterate through the levels starting from the last level and import all the pages at that level
for (int i = noOfLevels - 1; i >= 0; i--) {
final level = levels[i];
final filesAtLevel = level.pagesAtTheLevel;
final imagesAtlevel = level.assetsAtThelevel;
for (final file in filesAtLevel) {
final name = p.basenameWithoutExtension(file.page.name);
final String? pageID = await _createPage(
parentViewId,
file.page,
imagesAtlevel,
nameToId,
);
if (pageID == null) {
return;
}
nameToId[name] = pageID;
}
}
// In the end we will import the main page after we have imported all the
// subpages
final mainPageName = p.basenameWithoutExtension(mainpage.name);

final String? pageID = await _createPage(
parentViewId,
mainpage,
mainpageAssets,
nameToId,
);
if (pageID == null) {
return;
}
nameToId[mainPageName] = pageID;
// We have all the pages imported now we will move them under their
// respective parent page
for (int i = noOfLevels - 1; i >= 0; i--) {
final level = levels[i];
final filesAtLevel = level.pagesAtTheLevel;
for (final file in filesAtLevel) {
final name = p.basenameWithoutExtension(file.page.name);
final viewId = nameToId[name];
final parentName = file.parentName;
final parentID = nameToId[parentName];
await ViewBackendService.moveViewV2(
viewId: viewId!,
newParentId: parentID!,
prevViewId: parentViewId,
);
}
}
}

Future<String?> _createPage(
String parentViewId,
ArchiveFile file,
List<ArchiveFile> images,
Map<String, String> nameToID,
) async {
final name = p.basenameWithoutExtension(file.name);
final markdownContents = utf8.decode(file.content as Uint8List);
final processedMarkdownFile = await _preProcessMarkdownFile(
markdownContents,
images,
nameToID,
);
final data = documentDataFrom(
ImportType.markdownOrText,
Expand Down Expand Up @@ -202,6 +242,7 @@ class NotionImporter {
Future<String> _preProcessMarkdownFile(
String markdown,
Iterable<ArchiveFile> images,
Map<String, String> nameToID,
) async {
if (images.isEmpty) {
return markdown;
Expand All @@ -213,9 +254,7 @@ class NotionImporter {
if (line.isEmpty) {
continue;
}
if (!markdownImageRegex.hasMatch(line.trim())) {
result.add(line);
} else {
if (markdownImageRegex.hasMatch(line.trim())) {
final imagePath = markdownImageRegex.firstMatch(line)?.group(1);
if (imagePath == null) {
result.add(line);
Expand All @@ -232,6 +271,23 @@ class NotionImporter {
result.add(line.replaceFirst(imagePath, localPath));
}
}
} else if (fileRegex.hasMatch(line)) {
final String newLine = line.replaceAllMapped(fileRegex, (match) {
final String decodedFilePath = Uri.decodeFull(match.group(2)!);
if (!decodedFilePath.endsWith('.md')) {
return match.group(0)!;
}
final subpageName = p.basenameWithoutExtension(decodedFilePath);
final subPageID = nameToID[subpageName];
if (subPageID == null) {
return match.group(0)!;
}

return '{{AppFlowy-Subpage}}{$subpageName}{$subPageID}';
});
result.add(newLine);
} else {
result.add(line);
}
}
return result.join('\n');
Expand Down
8 changes: 4 additions & 4 deletions frontend/appflowy_flutter/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "56474e8b"
resolved-ref: "56474e8bd9f08be7090495c255eea20a694ab1f3"
ref: a9af2bb
resolved-ref: a9af2bbd373a6a478f1bd63d6037817e81d23de2
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "1.2.3"
version: "1.2.4"
appflowy_popover:
dependency: "direct main"
description:
Expand Down Expand Up @@ -810,7 +810,7 @@ packages:
source: hosted
version: "1.2.0"
markdown:
dependency: transitive
dependency: "direct main"
description:
name: markdown
sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd
Expand Down
5 changes: 3 additions & 2 deletions frontend/appflowy_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ dependencies:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: 56474e8b
ref: a9af2bb
appflowy_popover:
path: packages/appflowy_popover

Expand Down Expand Up @@ -91,6 +91,7 @@ dependencies:
charcode: ^1.3.1
collection: ^1.17.1
bloc: ^8.1.2
markdown: ^7.1.0
shared_preferences: ^2.1.1
google_fonts: ^4.0.5
percent_indicator: ^4.2.3
Expand All @@ -107,7 +108,7 @@ dependencies:
url_protocol:
hive: ^2.2.3
hive_flutter: ^1.1.0

dev_dependencies:
flutter_lints: ^2.0.1

Expand Down

0 comments on commit 94dc5b9

Please sign in to comment.