Skip to content

Commit

Permalink
feat: started markdown export feature
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasbach committed Mar 23, 2023
1 parent c678c56 commit bdb6d26
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 25 deletions.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
background-color: white;
}

pre {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"archiver": "^5.0.2",
"electron-updater": "^4.3.5",
"sqlite3": "^5.0.0",
"turndown": "^7.1.2",
"unzipper": "^0.10.11"
},
"devDependencies": {
Expand Down Expand Up @@ -73,6 +74,7 @@
"@types/react-dom": "^16.9.8",
"@types/rimraf": "^3.0.0",
"@types/sqlite3": "^3.1.6",
"@types/turndown": "^5.0.1",
"@types/unzip": "^0.1.1",
"@types/unzipper": "^0.10.3",
"@types/webpack": "^4.41.22",
Expand Down
78 changes: 78 additions & 0 deletions src/appdata/runMarkdownExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { DataItem, SearchResult, WorkSpace } from '../types';
import { DataInterface } from '../datasource/DataInterface';
import { DataSourceRegistry } from '../datasource/DataSourceRegistry';
import fs from 'fs-extra';
import path from 'path';
import { isCollectionItem, isMediaItem, isNoteItem } from '../utils';
import rimraf from 'rimraf';
import { InternalTag } from '../datasource/InternalTag';
import { EditorRegistry } from '../editors/EditorRegistry';

export type MarkdownExportOptions = {
targetFolder: string;
makeTextFilesToMarkdown: boolean;
}

export const runMarkdownExport = async (options: MarkdownExportOptions, workspace: WorkSpace) => {
console.log("Clearing...", options.targetFolder);
await new Promise(r => rimraf(options.targetFolder, r));
await fs.ensureDir(options.targetFolder);

const di = new DataInterface(
DataSourceRegistry.getDataSource(workspace),
EditorRegistry.Instance,
300
);

const items: DataItem[] = [];
let result: SearchResult = { nextPageAvailable: true, results: [] };
do {
result = await di.search({ all: true, limit: 200, pagingValue: result.nextPagingValue });
for (const item of result.results) {
items.push(item);
if (isMediaItem(item) && !item.referencePath) {
const mediaItem = await di.loadMediaItemContentAsPath(item.id);
console.log("media item", item, mediaItem);
} else if (isNoteItem(item)) {
const content = await di.getNoteItemContent(item.id);
console.log("note", item);
}
}
} while (result.nextPageAvailable);

const rootItem = items.find(item => item.tags.includes(InternalTag.WorkspaceRoot));
if (!rootItem) {
throw new Error("Root item not found");
}

await processItem(rootItem, "", options, di, items);


await di.unload();
}

const processItem = async (item: DataItem, parent: string, options: MarkdownExportOptions, di: DataInterface, items: DataItem[]) => {
console.log(`Exporting ${item.name} (${parent})...`);
const pathFriendlyName = item.name.replace(/[^a-zA-Z0-9- ]/g, "_");
const itemPath = path.join(options.targetFolder, parent);

if (isNoteItem(item)) {
const content = await di.getNoteItemContent<any>(item.id);
const dump = await EditorRegistry.Instance.getEditorWithId(item.noteType).exportContent(content, item, options);
const ext = options.makeTextFilesToMarkdown ? "md" : EditorRegistry.Instance.getEditorWithId(item.noteType).getExportFileExtension(content);
await fs.ensureDir(itemPath);
await fs.writeFile(path.join(itemPath, `${pathFriendlyName}.${ext}`), dump);
}

if (item.childIds.length > 0 && isCollectionItem(item)) {
const newParent = path.join(parent, pathFriendlyName);
await fs.ensureDir(path.join(itemPath, pathFriendlyName));

for (const child of item.childIds) {
const childItem = items.find(i => i.id === child);
if (childItem) {
await processItem(childItem, newParent, options, di, items);
}
}
}
}
41 changes: 41 additions & 0 deletions src/components/pages/ManageWorkspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { runImportWizard } from '../../appdata/runImportWizard';
import { runAddWorkspaceWizard } from '../../appdata/runAddWorkspaceWizard';
import { useScreenView } from '../telemetry/useScreenView';
import { Alerter } from '../Alerter';
import { runMarkdownExport } from '../../appdata/runMarkdownExport';
import { TelemetryService } from '../telemetry/TelemetryProvider';
import { TelemetryEvents } from '../telemetry/TelemetryEvents';

export const ManageWorkspaces: React.FC<{}> = props => {
useScreenView('manage-workspaces');
Expand Down Expand Up @@ -86,6 +89,44 @@ export const ManageWorkspaces: React.FC<{}> = props => {
}}
/>
</Tooltip>
<Tooltip content="Export workspace to markdown files" position={'bottom'}>
<Button
outlined
icon={'export'}
onClick={async () => {
const result = await remote.dialog.showOpenDialog({
buttonLabel: 'Export',
properties: ['createDirectory', 'openDirectory'],
title: 'Choose a location to export your workspace to',
});
if (result.filePaths) {
Alerter.Instance.alert({
content: (
<>
Warning: This will delete existing contents in <b>{result.filePaths[0]}</b> and
export the notebook there. Do you want to continue?
</>
),
intent: 'danger',
cancelButtonText: 'Cancel',
confirmButtonText: 'Continue',
prompt: {
type: "boolean",
defaultValue: false,
text: "Export code files as markdown files",
onConfirmBoolean: async (makeTextFilesToMarkdown) => {
await runMarkdownExport({
targetFolder: result.filePaths[0],
makeTextFilesToMarkdown
}, workspace);
remote.shell.showItemInFolder(result.filePaths[0]);
}
}
});
}
}}
/>
</Tooltip>
<Tooltip content="Delete workspace" position={'bottom'}>
<Button
outlined
Expand Down
3 changes: 3 additions & 0 deletions src/editors/EditorDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { NoteDataItem } from '../types';
import { MarkdownExportOptions } from '../appdata/runMarkdownExport';

export interface EditorComponentProps<C extends object> {
content: C;
Expand All @@ -21,4 +22,6 @@ export interface EditorDefinition<T extends string, C extends object> {
initializeContent: () => C;
editorComponent: React.FC<EditorComponentProps<C>>;
smallPreviewComponent?: React.FC<EditorSmallPreviewProps<C>>;
exportContent: (content: C, note: NoteDataItem<any>, options: MarkdownExportOptions) => Promise<string>;
getExportFileExtension: (content: C) => string;
}
25 changes: 0 additions & 25 deletions src/editors/atlassian-note-editor/AtlaskitNoteEditor.ts

This file was deleted.

76 changes: 76 additions & 0 deletions src/editors/atlassian-note-editor/AtlaskitNoteEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { EditorDefinition } from '../EditorDefinition';
import { EditorComponent } from './EditorComponent';
import { SmallPreviewComponent } from './SmallPreviewComponent';
import { ReactRenderer, renderDocument, ReactSerializer, TextSerializer } from '@atlaskit/renderer';
import ReactDOM from 'react-dom';
import Turndown from 'turndown';
import * as React from 'react';
import { NoteDataItem } from '../../types';

export interface AtlassianNoteEditorContent {
adf: any;
}

export class AtlaskitNoteEditor implements EditorDefinition<'atlaskit-editor-note', AtlassianNoteEditorContent> {
public id = 'atlaskit-editor-note' as const;
public name = 'Note';
public editorComponent = EditorComponent;
public smallPreviewComponent = SmallPreviewComponent;
public canInsertFiles = false;

public initializeContent(): AtlassianNoteEditorContent {
return {
adf: {
version: 1,
type: 'doc',
content: [],
},
};
}

public async exportContent(content: AtlassianNoteEditorContent, note: NoteDataItem<any>): Promise<string> {
const dumpElement = document.createElement('div');
dumpElement.style.display = 'none';
document.body.appendChild(dumpElement);
return new Promise<string>(r => {
ReactDOM.render(<ReactRenderer document={content.adf} />, dumpElement, () => {
setTimeout(() => {
// patch p elements from inside li elements
Array.from(dumpElement.getElementsByTagName("li")).forEach(li => {
if (li.children.length === 1 && li.children.item(0).tagName.toLowerCase() === "p") {
li.innerHTML = li.children[0].innerHTML;
}
});
// patch line numbers away from code blocks
Array.from(dumpElement.getElementsByClassName("linenumber")).forEach(item => {
item.outerHTML = "";
});
Array.from(dumpElement.querySelectorAll<HTMLElement>(".code-block code > span")).forEach(item => {
item.outerHTML = item.innerHTML;
});
Array.from(dumpElement.querySelectorAll<HTMLElement>(".code-block")).forEach(item => {

const code = item.querySelector<HTMLElement>("code").innerHTML;
item.innerHTML = `<pre><code>${code}</code></pre>`;
});


const text = dumpElement.innerHTML;
document.body.removeChild(dumpElement);
const td = new Turndown({
headingStyle: "atx",
hr: "---",
codeBlockStyle: "fenced",
});
const markdown = td.turndown(text);
const tags = note.tags.map(tag => `#${tag}`).join(' ');
r(tags ? `${markdown}\n\n${tags}` : markdown);
});
});
})
}

public getExportFileExtension(content: AtlassianNoteEditorContent): string {
return 'md';
}
}
29 changes: 29 additions & 0 deletions src/editors/monaco-note-editor/MonacoNoteEditor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { EditorDefinition } from '../EditorDefinition';
import { EditorComponent } from './EditorComponent';
import { AtlassianNoteEditorContent } from '../atlassian-note-editor/AtlaskitNoteEditor';
import { MarkdownExportOptions } from '../../appdata/runMarkdownExport';
import { NoteDataItem } from '../../types';

export interface MonacoNoteEditorContent {
content: string;
Expand All @@ -17,4 +20,30 @@ export class MonacoNoteEditor implements EditorDefinition<'monaco-editor-note',
content: '',
};
}

public async exportContent(content: MonacoNoteEditorContent, note: NoteDataItem<any>, options: MarkdownExportOptions): Promise<string> {
if (options.makeTextFilesToMarkdown) {
const fence = "```";
const tags = note.tags.map(tag => `#${tag}`).join(' ');
return `# ${note.name}\n${tags}\n\n${fence}${content.language || ''}\n${content.content}\n${fence}`;
}
return content.content;
}

public getExportFileExtension(content: MonacoNoteEditorContent): string {
switch(content.language) {
case 'typescript':
return 'ts';
case 'javascript':
return 'js';
case 'markdown':
return 'md';
case 'python':
return 'py';
case undefined:
return 'txt';
default:
return content.language;
}
}
}
13 changes: 13 additions & 0 deletions src/editors/todolist-note-editor/TodolistNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,17 @@ export class TodolistNoteEditor implements EditorDefinition<'todolist-editor-not
items: [],
};
}

public async exportContent(content: TodoListNoteEditorContent): Promise<string> {
return content.items.map(item => {
const tick = item.tickedOn ? '[x]' : '[ ]';
const descr = item.description ? `\n - ${item.description}` : '';
const steps = item.steps.map(step => `\n - ${step.title}`).join('');
return `${tick} ${item.title}${descr}${steps}`;
}).join('\n');
}

public getExportFileExtension(content: TodoListNoteEditorContent): string {
return 'md';
}
}
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3106,6 +3106,11 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74"
integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==

"@types/turndown@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.1.tgz#fcda7b02cda4c9d445be1440036df20f335b9387"
integrity sha512-N8Ad4e3oJxh9n9BiZx9cbe/0M3kqDpOTm2wzj13wdDUxDPjfjloWIJaquZzWE1cYTAHpjOH3rcTnXQdpEfS/SQ==

"@types/uglify-js@*":
version "3.9.3"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b"
Expand Down Expand Up @@ -5511,6 +5516,11 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"

domino@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.6.tgz#fe4ace4310526e5e7b9d12c7de01b7f485a57ffe"
integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==

[email protected]:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
Expand Down Expand Up @@ -11758,6 +11768,13 @@ tunnel@^0.0.6:
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==

turndown@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.1.2.tgz#7feb838c78f14241e79ed92a416e0d213e044a29"
integrity sha512-ntI9R7fcUKjqBP6QU8rBK2Ehyt8LAzt3UBT9JR9tgo6GtuKvyUzpayWmeMKJw1DPdXzktvtIT8m2mVXz+bL/Qg==
dependencies:
domino "^2.1.6"

tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
Expand Down

0 comments on commit bdb6d26

Please sign in to comment.