Skip to content

Commit

Permalink
Merge pull request #1 from blue-monk/feature/enhancement
Browse files Browse the repository at this point in the history
Enhancements, Bug Fixes, Add Testing Environment, and Miscellaneous Improvements
  • Loading branch information
blue-monk authored Apr 17, 2024
2 parents cfb5600 + 8da7300 commit 6c22e65
Show file tree
Hide file tree
Showing 24 changed files with 1,181 additions and 557 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,6 @@ dist
.pnp.*

output/*
!output/.gitkeep
!output/.gitkeep

testdata/*
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM node:latest

ENV SUT=quiver-to-obsidian-exporter-1.1.0.tgz

RUN apt-get update && apt-get install -y \
less \
&& rm -rf /var/lib/apt/lists/*

COPY testenv/.bashrc /root/.bashrc

WORKDIR /app

COPY $SUT /app

RUN npm install -g /app/$SUT

CMD ["/bin/bash"]
94 changes: 82 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,105 @@
# Quiver Library Markdown exporter to Obsidian
# Export Quiver library to Obsidian markdown files

![npm](https://img.shields.io/npm/v/quiver-markdown-exporter)
![npm](https://img.shields.io/npm/v/quiver-to-obsidian-exporter)

A [Quiver](https://yliansoft.com/) markdown note exporter. I started to use Quiver around 2015 and 2016. But I quickly get back to Evernote. Recently I found [Obsidian](https://obsidian.md/) quite useful and migrated all of my notes to it. So I write this exporter to migrate my notes from Quiver to Obsidian.
The original repository can be found
[here](https://github.com/Yukaii/quiver-markdown-exporter)
This command line tool is built upon the excellent foundation of the original repository.
Thank you!

And my Quiver notes only use a subset of Quiver features, so I can't guarantee that the exported notes are the same as the ones in Quiver. But I hope it's enough for you to get started.
---

Some working features:
This tool facilitates migration from Quiver to Obsidian.
I've enhanced its features and corrected several bugs, as the original functionality did not fully meet my needs.

Please note that both the repository name and the command name have been changed for clarity.

![App Concept Image](app-concept-image.png)
![App Running Image](app-running.png)

[Quiver](https://yliansoft.com/)
[Obsidian](https://obsidian.md/)

- [Attachments](https://github.com/Yukaii/quiver-obsidian-markdown/blob/c13f42daa8af30268797b3d902ba9f844bc24873/src/quiver-markdown.mts#L24-L29)
- Images, [\[1\]](https://github.com/Yukaii/quiver-obsidian-markdown/blob/c13f42daa8af30268797b3d902ba9f844bc24873/src/quiver-markdown.mts#L32-L46), and [\[2\]](https://github.com/Yukaii/quiver-obsidian-markdown/blob/c13f42daa8af30268797b3d902ba9f844bc24873/src/quiver-markdown.mts#L95-L103A)
- [Diagrams and Markdown/Code block](https://github.com/Yukaii/quiver-obsidian-markdown/blob/c13f42daa8af30268797b3d902ba9f844bc24873/src/quiver-markdown.mts#L117-L120)

## Installation

```bash
npm install -g quiver-markdown-exporter
npm install -g quiver-to-obsidian-exporter
```

## Usage

```bash
Usage
$ quiver-markdown <input.qvlibrary> -o <output folder>
$ qvr2obs <input.qvlibrary> -o <output folder> -a <Attachment folder policy>
or
$ qvr2obs <input.qvlibrary> -o <output folder> -a <Attachment folder policy> -n <Attachment subfolder name if needed>

Options
--output, -o Output folder
--output, -o: Output folder
--attachmentFolderPolicy, -a: Attachment folder policy (vaultFolder, subfolderUnderVault, sameFolderAsEachFile, subfolderUnderEachFolder). 'subfolderUnderVault' and 'subfolderUnderEachFolder' require subfolder name.
--attachmentSubfolderName, -n: Specify the subfolder name if 'subfolderUnderVault' or 'subfolderUnderEachFolder' is selected as the attachmentFolderPolicy option.

Examples
$ quiver-markdown MyLibrary.qvlibrary -o dist
$ qvr2obs MyNote.qvlibrary -o dest/MyNote -a vaultFolder
$ qvr2obs MyNote.qvlibrary -o dest/MyNote -a subfolderUnderVault -n _attachments
```
## Changes from the Original
### New Features
* Output maintains the tree structure of the Quiver library (originally output was flat).
* Added support for all four Obsidian attachment folder policies.
* Converts tags, creation and modification times of notebooks into YAML front matter.
* Migrates Quiver notebooks while preserving their timestamps.
* Added a progress bar to display the transformation progress from Quiver to Obsidian.
### Bug Fixes
* Fixed an issue where image links in Markdown cells were not rendering in Obsidian.
* Sanitized characters in titles that are not allowed in Obsidian.
* Fixed LaTeX rendering issues to ensure correct display in Obsidian.
### Minor Changes
* Changed the timestamp formatter to `YYYY-MM-DD(ddd) HH:mm:ss`.
* Added a check for the existence of the qvlibrary file.
* Displays help text when executed without arguments.
### For Developers
* Added debug logging (controlled by the environment variable QUIVER_TO_OBSIDIAN_EXPORTER_LOGGING_VERBOSE).
* Created a Docker environment dedicated to testing.
## How to Test (For Developers)
This testing procedure is designed for testing in a clean environment.
For routine testing, feel free to use your IDE of choice.
1. Prepare the `testdata` folder:
In the testdata folder, place xxx.qvlibrary in the sources directory, for example, and also provide a destination folder, etc. and use it as the location for the -o option (-o testdata/destination/MyNote)
2. `yarn run build`.
3. `npm pack`.
4. `docker compose up -d --build`.
5. Enter the Docker container:
e.g.
```
docker exec -it quiver-to-obsidian-exporter-app-1 /bin/bash
```
6. Execute the command:
e.g.
```
qvr2obs testdata/source/MyNote.qvlibrary -o testdata/destination/MyNote -a subfolderUnderVault -n _attachments
```
If needed, enable verbose logging for debugging:
```
export QUIVER_TO_OBSIDIAN_EXPORTER_LOGGING_VERBOSE=true
```
## Contributing
Expand Down
Binary file added app-concept-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app-running.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
app:
image: quiver-to-obsidian-exporter
build:
context: .
dockerfile: Dockerfile
volumes:
- ./testdata:/app/testdata
command: tail -f /dev/null
38 changes: 25 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
{
"name": "quiver-markdown-exporter",
"version": "1.0.1",
"name": "quiver-to-obsidian-exporter",
"version": "1.1.0",
"description": "Export Quiver library to Obsidian markdown files",
"repository": {
"type": "git",
"url": "https://github.com/blue-monk/quiver-to-obsidian-exporter"
},
"homepage": "https://github.com/blue-monk/quiver-to-obsidian-exporter",
"main": "dist/index.mjs",
"files": [
"dist"
],
"type": "module",
"bin": {
"quiver-markdown": "dist/index.mjs"
"qvr2obs": "dist/index.mjs"
},
"engines": {
"node": ">=21.7.3"
},
"scripts": {
"build": "tsc",
"build": "rm -rf ./dist && tsc",
"prepublishOnly": "npm run build"
},
"keywords": [],
"author": "",
"license": "ISC",
"license": "MIT",
"devDependencies": {
"@types/node": "^17.0.32",
"@types/turndown": "^5.0.1",
"ts-node": "^10.7.0",
"typescript": "^4.8.0-dev.20220512"
"@types/node": "^20.12.7",
"@types/turndown": "^5.0.4",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"dependencies": {
"fast-glob": "^3.2.11",
"fs-extra": "^10.1.0",
"meow": "^10.1.2",
"turndown": "^7.1.1"
"chalk": "^5.3.0",
"cli-progress": "^3.12.0",
"dayjs": "^1.11.10",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
"meow": "^13.2.0",
"turndown": "^7.1.3",
"utimes": "^5.2.1"
}
}
20 changes: 20 additions & 0 deletions src/assertions.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import fs from "fs-extra";

import { getLogger } from './logger.mjs';


const logger = getLogger();


export function assertValidQvlibraryPath(qvlibraryPath: string) {

if (!fs.existsSync(qvlibraryPath)) {
logger.error(`The specified qvlibraryPath does not exist. [qvlibraryPath=${qvlibraryPath}]`);
process.exit(2);
}

if (!fs.statSync(qvlibraryPath).isDirectory()) {
logger.error(`The specified qvlibraryPath is not a directory. [qvlibraryPath=${qvlibraryPath}]`);
process.exit(2);
}
}
18 changes: 18 additions & 0 deletions src/extensions/String+Path.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@


declare global {
interface String {
lastPathComponent(): string;
}
}


if (!String.prototype.lastPathComponent) {
String.prototype.lastPathComponent = function () {

const pathComponents = this.split('/');
return pathComponents[pathComponents.length - 1];
};
}

export {};
103 changes: 88 additions & 15 deletions src/index.mts
Original file line number Diff line number Diff line change
@@ -1,34 +1,107 @@
#!/usr/bin/env node
import meow from 'meow';
import { convert } from './quiver-markdown.mjs';

const cli = meow(`
Usage
$ quiver-markdown <input.qvlibrary> -o <output folder>
import { getLogger } from './logger.mjs';
import { assertValidQvlibraryPath } from './assertions.mjs';
import { exportQvlibrary } from './quiver-to-obsidian-exporter.mjs';
import { AttachmentFolderPolicy, createAttachmentFolderPolicyWithSubfolder, createAttachmentFolderPolicyWithoutSubfolder } from './migration-support/attachment-folder-treatment.mjs';

Options
--output, -o Output folder

Examples
$ quiver-markdown MyLibrary.qvlibrary -o dist
`, {
const logger = getLogger();


const helpText = `
Usage
$ qvr2obs <input.qvlibrary> -o <output folder> -a <Attachment folder policy>
or
$ qvr2obs <input.qvlibrary> -o <output folder> -a <Attachment folder policy> -n <Attachment subfolder name if needed>
Options
--output, -o: Output folder
--attachmentFolderPolicy, -a: Attachment folder policy (vaultFolder, subfolderUnderVault, sameFolderAsEachFile, subfolderUnderEachFolder). 'subfolderUnderVault' and 'subfolderUnderEachFolder' require subfolder name.
--attachmentSubfolderName, -n: Specify the subfolder name if 'subfolderUnderVault' or 'subfolderUnderEachFolder' is selected as the attachmentFolderPolicy option.
Examples
$ qvr2obs MyNote.qvlibrary -o dest/MyNote -a vaultFolder
$ qvr2obs MyNote.qvlibrary -o dest/MyNote -a subfolderUnderVault -n _attachments
`


const args = process.argv.slice(2)
if (args.length === 0) {
meow(helpText, { importMeta: import.meta }).showHelp();
}

const cli = meow(helpText, {
importMeta: import.meta,
flags: {
output: {
type: 'string',
alias: 'o',
shortFlag: 'o',
isRequired: true,
},
attachmentFolderPolicy: {
type: 'string',
choices: ['vaultFolder', 'subfolderUnderVault', 'sameFolderAsEachFile', 'subfolderUnderEachFolder'],
shortFlag: 'a',
isRequired: true,
},
attachmentSubfolderName: {
type: 'string',
shortFlag: 'n',
isRequired: (flags, input) => {
return (flags.attachmentFolderPolicy as string)?.startsWith('subfolder');
},
},
},
});

if (cli.input.length < 1) {
console.error('Please provide a qvlibrary file');
process.exit(1);
logger.error('Please provide a qvlibrary file');
cli.showHelp();
}

if (!cli.flags.output) {
console.error('Please provide an output folder');
process.exit(1);
logger.error('Please provide an output folder');
cli.showHelp();
}

convert(cli.input[0], cli.flags.output);
if (cli.flags.attachmentFolderPolicy.startsWith('subfolder')) {
if (!cli.flags.attachmentSubfolderName) {
logger.error(`Please provide an Attachment subfolder name with -n option (Because you specified '${cli.flags.attachmentFolderPolicy}' with -i)`);
cli.showHelp();
}
}
else {
if (cli.flags.attachmentSubfolderName) {
logger.error(`It is not necessary to specify Attachment subfolder name (Because you specified '${cli.flags.attachmentFolderPolicy}' with -i) (or is it a mistake in specifying attachmentFolderPolicy?)`);
cli.showHelp();
}
}



const qvlibraryPath = cli.input[0]
assertValidQvlibraryPath(qvlibraryPath);

const outputPath = cli.flags.output

const attachmentFolderPolicy = createAttachmentFolderPolicy(cli.flags.attachmentFolderPolicy, cli.flags.attachmentSubfolderName)
logger.info(`AttachmentFolderPolicy: { attachmentFolderPolicy=${cli.flags.attachmentFolderPolicy}, attachmentSubfolderName=${cli.flags.attachmentSubfolderName} }`);

exportQvlibrary(qvlibraryPath, outputPath, attachmentFolderPolicy);


function createAttachmentFolderPolicy(policyType: string, subfolderName: string): AttachmentFolderPolicy {

switch (policyType) {
case 'vaultFolder':
case 'sameFolderAsEachFile':
return createAttachmentFolderPolicyWithoutSubfolder(policyType);
case 'subfolderUnderVault':
case 'subfolderUnderEachFolder':
return createAttachmentFolderPolicyWithSubfolder(policyType, subfolderName);
default:
throw new Error(`Unknown policy type: ${policyType}`);
}
}
Loading

0 comments on commit 6c22e65

Please sign in to comment.