quiver-to-obsidian
Version:
Convert quiver libraray to obsidian library
246 lines • 12.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import fse from 'fs-extra';
import * as path from 'path';
import ProgressBar from 'progress';
import ora from 'ora';
import TurndownService from 'turndown';
import { utimes } from 'utimes';
import { CellType, } from './type.js';
import { readLibrary, walkThroughNotebookHierarchty, readNoteContent } from './quiver_parse.js';
import { checkOutputDirPath, prepareDirectory, newDistinctNoteName } from './utils.js';
class Quiver {
constructor(library, extNames) {
this.newNotePathRecord = {};
this.outputQuiverPath = '';
this.noteCount = 0;
this.library = library;
if (extNames && extNames.length > 0) {
this.needReplaceExtNames = extNames;
}
}
static newQuiver(libraryPath, extNames) {
return __awaiter(this, void 0, void 0, function* () {
const spinner = ora('Loading library...').start();
const library = yield readLibrary(libraryPath);
const quiver = new Quiver(library, extNames);
spinner.stop();
return quiver;
});
}
transformQvLibraryToObsidian(outputPath) {
return __awaiter(this, void 0, void 0, function* () {
checkOutputDirPath(outputPath);
// add `quiver` to output path
this.outputQuiverPath = path.join(outputPath, 'quiver');
this.spinner = ora('Reading library...').start();
this.walkThroughNotebookHierarchty((notebook, parents) => {
const newPathList = [this.outputQuiverPath];
parents.forEach((parentNotebook) => {
newPathList.push(parentNotebook.meta.name);
});
newPathList.push(notebook.meta.name);
const newPath = path.join(...newPathList);
// Prevent file name conflicts
const noteNames = [];
notebook.notes.forEach((note) => {
if (this.newNotePathRecord[note.meta.uuid]) {
throw new Error(`there has two notes with uuid(${note.meta.uuid}), please check and try again`);
}
let noteName = note.meta.title;
if (noteNames.indexOf(noteName) > -1) {
noteName = newDistinctNoteName(noteName, noteNames, 2);
}
noteNames.push(noteName);
this.newNotePathRecord[note.meta.uuid] = path.join(newPath, `${noteName}.md`);
});
});
this.noteCount = Object.keys(this.newNotePathRecord).length;
this.bar = new ProgressBar('Processing [:bar] :current/:total', { total: this.noteCount });
yield this.writeLibrary();
return this.outputQuiverPath;
});
}
walkThroughNotebookHierarchty(callback) {
var _a;
const notebooks = {};
this.library.notebooks.forEach((notebook) => {
notebooks[notebook.meta.uuid] = notebook;
});
const parents = [];
(_a = this.library.meta.children) === null || _a === void 0 ? void 0 : _a.forEach((meta) => {
walkThroughNotebookHierarchty(meta, parents, (notebookName, parentNames) => {
const parentNotebooks = [];
parentNames.forEach((name) => {
parentNotebooks.push(notebooks[name]);
});
callback(notebooks[notebookName], parentNotebooks);
});
});
}
writeLibrary() {
return __awaiter(this, void 0, void 0, function* () {
const notebookInfoList = [];
this.walkThroughNotebookHierarchty((notebook, parents) => {
const newPathList = [this.outputQuiverPath];
parents.forEach((parentNotebook) => {
newPathList.push(parentNotebook.meta.name);
});
newPathList.push(notebook.meta.name);
const newNotebookPath = path.join(...newPathList);
notebookInfoList.push({ notebook, notebookPath: newNotebookPath });
});
yield Promise.all(notebookInfoList.map((n) => __awaiter(this, void 0, void 0, function* () {
yield this.writeNotebook(n.notebook, n.notebookPath);
})));
});
}
writeNotebook(notebook, newNotebookPath) {
return __awaiter(this, void 0, void 0, function* () {
prepareDirectory(newNotebookPath);
yield Promise.all(notebook.notes.map((note) => __awaiter(this, void 0, void 0, function* () {
const notePath = this.newNotePathRecord[note.meta.uuid];
yield this.writeNote(note, notePath);
})));
});
}
writeNote(note, newNotePath) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
yield this.writeNoteToMarkdown(note, newNotePath);
if (note.resources) {
const resourceDirPath = path.join(this.outputQuiverPath, 'resources');
prepareDirectory(resourceDirPath);
yield Promise.all(note.resources.files.map((file) => __awaiter(this, void 0, void 0, function* () {
const fileName = this.replaceResourceName(file.name, true);
const srcPath = path.join(note.notePath, 'resources', file.name);
const dstPath = path.join(resourceDirPath, fileName);
yield fse.copyFile(srcPath, dstPath);
})));
}
if (this.spinner) {
this.spinner.stop();
this.spinner = undefined;
}
(_a = this.bar) === null || _a === void 0 ? void 0 : _a.tick();
});
}
// transform note content to markdown and convert TextCell to markdown
writeNoteToMarkdown(note, notePath) {
return __awaiter(this, void 0, void 0, function* () {
let fd;
try {
yield fse.createFile(notePath);
fd = yield fse.promises.open(notePath, 'w+');
const noteContent = yield readNoteContent(note.contentPath);
noteContent.cells.forEach((cell, i) => {
var _a;
if (i !== 0) {
fd === null || fd === void 0 ? void 0 : fd.write('\n\n');
}
const { data } = cell;
switch (cell.type) {
case CellType.MarkdownCell: {
const transformData = this.transformQuiverResourceAndNoteLink(data);
fd === null || fd === void 0 ? void 0 : fd.write(transformData);
break;
}
case CellType.TextCell: {
const turndownService = new TurndownService();
const markdown = turndownService.turndown(data);
const transformData = this.transformQuiverResourceAndNoteLink(markdown);
fd === null || fd === void 0 ? void 0 : fd.write(transformData);
break;
}
case CellType.CodeCell: {
const language = (_a = cell.language) !== null && _a !== void 0 ? _a : '';
fd === null || fd === void 0 ? void 0 : fd.write(`\`\`\`${language}\n${data}\n\`\`\``);
break;
}
case CellType.LatexCell: {
fd === null || fd === void 0 ? void 0 : fd.write(`\`\`\`latex\n${data}\n\`\`\``);
break;
}
case CellType.DiagramCell: {
let tool = 'Sequence diagram, see https://bramp.github.io/js-sequence-diagrams';
if (cell.diagramType === 'flow') {
tool = 'Flowchart diagram, see http://flowchart.js.org';
}
fd === null || fd === void 0 ? void 0 : fd.write(`\`\`\`javascript\n// ${tool}\n${data}\`\`\``);
break;
}
default:
break;
}
});
}
catch (error) {
throw error;
}
finally {
if (fd) {
fd.close();
}
}
try {
// rewrite create time and update time of md file
yield utimes(notePath, {
btime: Number(note.meta.created_at * 1000),
mtime: Number(note.meta.updated_at * 1000),
atime: 0,
});
}
catch (error) {
// ignore
}
});
}
// transform quiver resource and note link url
transformQuiverResourceAndNoteLink(data) {
let transformData = data.replace(/quiver-image-url\//g, 'resources/');
transformData = transformData.replace(/quiver-file-url\//g, 'resources/');
transformData = this.replaceResourceName(transformData, false);
// replace note link in content to obsidian link
// eslint-disable-next-line max-len
transformData = transformData.replace(/\[.*?\]\((quiver-note-url|quiver:\/\/\/notes)\/([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\)/g, (_, __, uuid) => {
const linkNotePath = this.newNotePathRecord[uuid];
const linkNoteName = path.basename(linkNotePath);
return ` [[${linkNoteName}]]`;
});
return transformData;
}
// rename resource file names and links
replaceResourceName(data, isForFile) {
const resourceTag = isForFile ? '' : 'resources/';
const prefix = isForFile ? '^' : '\\(';
const suffix = isForFile ? '$' : '\\)';
// 1.remove url args from img file name, like:
// ``
// `9FFEF50881EA1326EA55C1BC43EC9314.png&w=2048&q=75`
// `55F20500B6E0C67E3EA78ED6C149B4D9.svg?style=social&label=Follow%20on%20Twitter`
// eslint-disable-next-line max-len
const clearSuffixReg = new RegExp(`${prefix}(${resourceTag}.*?\\.(bmp|jpg|png|tif|gif|pcx|tga|exif|fpx|svg|psd|cdr|pcd|dxf|ufo|eps|ai|raw|WMF|webp|jpeg|ico|awebp))(\\s|&|\\?).*${suffix}`, 'gi');
let transformData = data.replace(clearSuffixReg, (_, group1) => (isForFile ? group1 : `(${group1})`));
// replace unknown image file ext to `png`
if (this.needReplaceExtNames && this.needReplaceExtNames.length > 0) {
const replaceExt = this.needReplaceExtNames.join('|');
// eslint-disable-next-line max-len
const renameAwebpReg = new RegExp(`${prefix}(${resourceTag}.*?)\\.(${replaceExt})${suffix}`, 'gi');
transformData = transformData.replace(renameAwebpReg, (_, group1) => (isForFile ? `${group1}.png` : `(${group1}.png)`));
}
// add default ext (png) for none ext resource file like`(resources/BC8755B05A094564A25EA19E438B73B3)`
// eslint-disable-next-line max-len
const addDefaultExtReg = new RegExp(`${prefix}(${resourceTag}[0-9A-Z]{32})${suffix}`, 'g');
transformData = transformData.replace(addDefaultExtReg, (_, group1) => (isForFile ? `${group1}.png` : `(${group1}.png)`));
return transformData;
}
}
export default Quiver;
//# sourceMappingURL=index.js.map