Skip to content

Commit

Permalink
add support for turtl
Browse files Browse the repository at this point in the history
  • Loading branch information
marph91 committed Oct 17, 2024
1 parent 68124d1 commit dc8360e
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 8 deletions.
5 changes: 5 additions & 0 deletions docs/contributing/get_sample_exports.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ Documentation how to get sample exports from "difficult" apps. Difficult means t
1. Go to the [demo page](https://demo.synology.com/de-de/dsm) and press the "Test" button
2. Open Note Station by Apps -> Note Station on the top left
3. Files can be transferred through the file system

## Turtl

- Linux app crashes always
- Android app still works
3 changes: 1 addition & 2 deletions docs/contributing/more_note_apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ Loose collection of note apps/messengers/wikis/formats that could be implemented
| Treepad | [thread](https://discourse.joplinapp.org/t/how-can-i-export-html-notes-from-treepad/27554) | [dead](https://www.myinfoapp.com/blog/what-happened-to-treepad) |
| [Trello](https://trello.com/) | [doc](https://support.atlassian.com/trello/docs/exporting-data-from-trello/) (json) | |
| [Trilium](https://github.com/zadam/trilium) | [thread](https://github.com/zadam/trilium/discussions/2827) (Markdown?) | |
| [Turtl](https://turtlapp.com/) | [thread](https://discourse.joplinapp.org/t/turtl-to-md-directory-python/29010) | dead |
| [Twos](https://www.twosapp.com/) | [export](https://www.twosapp.com/export) (markdown, only single notes?) | |
| [Ulysses](https://ulysses.app/) | [thread](https://github.com/obsidianmd/obsidian-importer/issues/18) (Markdown) | |
| [UpNote](https://getupnote.com/) | [doc](https://medium.com/upnote/export-your-notes-3d8d6f7739d7) (Markdown, paid only) | |
Expand All @@ -92,6 +91,6 @@ Loose collection of note apps/messengers/wikis/formats that could be implemented
| [Wiznote](https://www.wiz.cn/) | [script](https://github.com/scher000/wiz2joplin) | |
| [Workflowy](https://workflowy.com/) | [doc](https://workflowy.com/help/exporting/) (unusable?) | |
| Wunderlist | [script](https://github.com/eschlot/Wunderlist2Joplin) | dead? |
| [Xiaomi Notes](https://i.mi.com/note/h5) | | account needed |
| [Xiaomi Notes](https://i.mi.com/note/h5) | | - account needed <br>- [export possible?](https://www.reddit.com/r/Xiaomi/comments/je76bz/is_there_any_way_to_export_all_your_notes_stored/) |
| [XWiki](https://www.xwiki.org/) | [doc](https://www.xwiki.org/xwiki/bin/view/Documentation/UserGuide/Features/Exports) | |
| [Zotero](https://www.zotero.org/) | [doc](https://www.zotero.org/support/kb/exporting) | |
20 changes: 20 additions & 0 deletions docs/formats/turtl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
This page describes how to convert notes from Turtl to Markdown.

## General Information

- [Website](https://turtlapp.com/)
- Typical extension: `.json`

## Instructions

1. Export as shown [at the website](https://turtlapp.com/features/)
2. [Install jimmy](../index.md#installation)
3. Convert to Markdown. Example: `jimmy-cli-linux turtl-backup.json --format turtl`
4. [Import to your app](../import_instructions.md)

## Import Structure

- Spaces are converted to folders.
- Boards are converted to subfolders of the corresponding space.
- Notes are converted to Markdown files in the corresponding board.
- Tags and files are converted.
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Export data from your app and convert it to Markdown. For details, click on the
||||||
| :---: | :---: | :---: | :---: | :---: |
| <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Anki-icon.svg/240px-Anki-icon.svg.png" style="height:100px;max-width:100px;"><br>[Anki](https://marph91.github.io/jimmy/formats/anki/) | <img src="https://bear.app/images/logo.png" style="height:100px;max-width:100px;"><br>[Bear](https://marph91.github.io/jimmy/formats/bear/) | <img src="https://raw.githubusercontent.com/CacherApp/cacher-cli/e241f06867dba740131db5314ef7fe279135baf6/images/cacher-icon.png" style="height:100px;max-width:100px;"><br>[Cacher](https://marph91.github.io/jimmy/formats/cacher/) | <img src="https://raw.githubusercontent.com/giuspen/cherrytree/c822b16681b002b8882645d8d1e8f109514ddb58/icons/cherrytree.svg" style="height:100px;max-width:100px;"><br>[CherryTree](https://marph91.github.io/jimmy/formats/cherrytree/) | <img src="https://avatars.githubusercontent.com/u/53916365?s=200&v=4" style="height:100px;max-width:100px;"><br>[Clipto](https://marph91.github.io/jimmy/formats/clipto/) |
| <img src="https://www.colornote.com/wp-content/uploads/2016/05/cropped-favicon.png" style="height:100px;max-width:100px;"><br>[ColorNote](https://marph91.github.io/jimmy/formats/colornote/) | | | | |
| <img src="https://www.colornote.com/wp-content/uploads/2016/05/cropped-favicon.png" style="height:100px;max-width:100px;"><br>[ColorNote](https://marph91.github.io/jimmy/formats/colornote/) | <img src="https://turtlapp.com/images/logo.svg" style="height:100px;max-width:100px;"><br>[Turtl](https://marph91.github.io/jimmy/formats/turtl/) | | | |
| <img src="https://seeklogo.com/images/D/day-one-logo-F4CA245C26-seeklogo.com.png" style="height:100px;max-width:100px;"><br>[Day&nbsp;One](https://marph91.github.io/jimmy/formats/day_one/) | <img src="https://images.saasworthy.com/dynalist_5288_logo_1576239391_xhkcg.jpg" style="height:100px;max-width:100px;"><br>[Dynalist](https://marph91.github.io/jimmy/formats/dynalist/) | <img src="https://upload.wikimedia.org/wikipedia/commons/b/b8/2021_Facebook_icon.svg" style="height:100px;max-width:100px;"><br>[Facebook](https://marph91.github.io/jimmy/formats/facebook/) | <img src="https://wavebox.pro/store2/store/0b46bf0a-107c-4fa2-a657-3df7412e3d3d.png" style="height:100px;max-width:100px;"><br>[FuseBase, Nimbus&nbsp;Note](https://marph91.github.io/jimmy/formats/fusebase/) | <img src="https://www.gstatic.com/images/branding/product/1x/docs_2020q4_96dp.png" style="height:100px;max-width:100px;"><br>[Google&nbsp;Docs](https://marph91.github.io/jimmy/formats/google_docs/) |
| <img src="https://www.gstatic.com/images/branding/product/1x/keep_2020q4_96dp.png" style="height:100px;max-width:100px;"><br>[Google&nbsp;Keep](https://marph91.github.io/jimmy/formats/google_keep/) | <img src="https://github.com/laurent22/joplin/blob/dev/Assets/LinuxIcons/128x128.png?raw=true" style="height:100px;max-width:100px;"><br>[Joplin](https://marph91.github.io/jimmy/formats/joplin/) | <img src="https://raw.githubusercontent.com/jrnl-org/jrnl/85a98afcd91ed873c0eceba9893c3ec424f201b8/docs_theme/img/logo.svg" style="height:100px;max-width:100px;"><br>[jrnl](https://marph91.github.io/jimmy/formats/jrnl/) | <img src="https://upload.wikimedia.org/wikipedia/commons/4/45/Notion_app_logo.png" style="height:100px;max-width:100px;"><br>[Notion](https://marph91.github.io/jimmy/formats/notion/) | <img src="https://upload.wikimedia.org/wikipedia/commons/1/10/2023_Obsidian_logo.svg" style="height:100px;max-width:100px;"><br>[Obsidian](https://marph91.github.io/jimmy/formats/obsidian/) |
| <img src="https://raw.githubusercontent.com/pbek/QOwnNotes/d89a597a28eeb16f57692ac121933b478f44bf07/src/images/icons/256x256/apps/QOwnNotes.png" style="height:100px;max-width:100px;"><br>[QOwnNotes](https://marph91.github.io/jimmy/formats/qownnotes/) | <img src="https://raw.githubusercontent.com/jendrikseipp/rednotebook/b2cefe5f321b21ab7ad855059f3c0496eb0830d2/rednotebook/images/rednotebook-icon/rn-256.png" style="height:100px;max-width:100px;"><br>[RedNotebook](https://marph91.github.io/jimmy/formats/rednotebook/) | <img src="https://raw.githubusercontent.com/Automattic/simplenote-electron/4a140a96545763c849b26a81a2e27ff67eaa68f0/lib/icons/app-icon/icon_256x256.png" style="height:100px;max-width:100px;"><br>[Simplenote](https://marph91.github.io/jimmy/formats/simplenote/) | <img src="https://avatars.githubusercontent.com/u/24537496?s=100" style="height:100px;max-width:100px;"><br>[Standard&nbsp;Notes](https://marph91.github.io/jimmy/formats/standard_notes/) | <img src="https://www.synology.com/img/dsm/note_station/notestation_72.png" style="height:100px;max-width:100px;"><br>[Synology Note&nbsp;Station](https://marph91.github.io/jimmy/formats/synology_note_station/) |
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ nav:
# - Todoist: formats/todoist.md
- Tomboy-ng: formats/tomboy_ng.md
# - Toodledo: formats/toodledo.md
- Turtl: formats/turtl.md
- vCard: formats/vcard.md
# - xit: formats/xit.md
- Zettelkasten: formats/zettelkasten.md
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Export data from your app and convert it to Markdown. For details, click on the
||||||
| :---: | :---: | :---: | :---: | :---: |
| <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Anki-icon.svg/240px-Anki-icon.svg.png" style="height:100px;max-width:100px;"><br>[Anki](https://marph91.github.io/jimmy/formats/anki/) | <img src="https://bear.app/images/logo.png" style="height:100px;max-width:100px;"><br>[Bear](https://marph91.github.io/jimmy/formats/bear/) | <img src="https://raw.githubusercontent.com/CacherApp/cacher-cli/e241f06867dba740131db5314ef7fe279135baf6/images/cacher-icon.png" style="height:100px;max-width:100px;"><br>[Cacher](https://marph91.github.io/jimmy/formats/cacher/) | <img src="https://raw.githubusercontent.com/giuspen/cherrytree/c822b16681b002b8882645d8d1e8f109514ddb58/icons/cherrytree.svg" style="height:100px;max-width:100px;"><br>[CherryTree](https://marph91.github.io/jimmy/formats/cherrytree/) | <img src="https://avatars.githubusercontent.com/u/53916365?s=200&v=4" style="height:100px;max-width:100px;"><br>[Clipto](https://marph91.github.io/jimmy/formats/clipto/) |
| <img src="https://www.colornote.com/wp-content/uploads/2016/05/cropped-favicon.png" style="height:100px;max-width:100px;"><br>[ColorNote](https://marph91.github.io/jimmy/formats/colornote/) | | | | |
| <img src="https://www.colornote.com/wp-content/uploads/2016/05/cropped-favicon.png" style="height:100px;max-width:100px;"><br>[ColorNote](https://marph91.github.io/jimmy/formats/colornote/) | <img src="https://turtlapp.com/images/logo.svg" style="height:100px;max-width:100px;"><br>[Turtl](https://marph91.github.io/jimmy/formats/turtl/) | | | |
| <img src="https://seeklogo.com/images/D/day-one-logo-F4CA245C26-seeklogo.com.png" style="height:100px;max-width:100px;"><br>[Day&nbsp;One](https://marph91.github.io/jimmy/formats/day_one/) | <img src="https://images.saasworthy.com/dynalist_5288_logo_1576239391_xhkcg.jpg" style="height:100px;max-width:100px;"><br>[Dynalist](https://marph91.github.io/jimmy/formats/dynalist/) | <img src="https://upload.wikimedia.org/wikipedia/commons/b/b8/2021_Facebook_icon.svg" style="height:100px;max-width:100px;"><br>[Facebook](https://marph91.github.io/jimmy/formats/facebook/) | <img src="https://wavebox.pro/store2/store/0b46bf0a-107c-4fa2-a657-3df7412e3d3d.png" style="height:100px;max-width:100px;"><br>[FuseBase, Nimbus&nbsp;Note](https://marph91.github.io/jimmy/formats/fusebase/) | <img src="https://www.gstatic.com/images/branding/product/1x/docs_2020q4_96dp.png" style="height:100px;max-width:100px;"><br>[Google&nbsp;Docs](https://marph91.github.io/jimmy/formats/google_docs/) |
| <img src="https://www.gstatic.com/images/branding/product/1x/keep_2020q4_96dp.png" style="height:100px;max-width:100px;"><br>[Google&nbsp;Keep](https://marph91.github.io/jimmy/formats/google_keep/) | <img src="https://github.com/laurent22/joplin/blob/dev/Assets/LinuxIcons/128x128.png?raw=true" style="height:100px;max-width:100px;"><br>[Joplin](https://marph91.github.io/jimmy/formats/joplin/) | <img src="https://raw.githubusercontent.com/jrnl-org/jrnl/85a98afcd91ed873c0eceba9893c3ec424f201b8/docs_theme/img/logo.svg" style="height:100px;max-width:100px;"><br>[jrnl](https://marph91.github.io/jimmy/formats/jrnl/) | <img src="https://upload.wikimedia.org/wikipedia/commons/4/45/Notion_app_logo.png" style="height:100px;max-width:100px;"><br>[Notion](https://marph91.github.io/jimmy/formats/notion/) | <img src="https://upload.wikimedia.org/wikipedia/commons/1/10/2023_Obsidian_logo.svg" style="height:100px;max-width:100px;"><br>[Obsidian](https://marph91.github.io/jimmy/formats/obsidian/) |
| <img src="https://raw.githubusercontent.com/pbek/QOwnNotes/d89a597a28eeb16f57692ac121933b478f44bf07/src/images/icons/256x256/apps/QOwnNotes.png" style="height:100px;max-width:100px;"><br>[QOwnNotes](https://marph91.github.io/jimmy/formats/qownnotes/) | <img src="https://raw.githubusercontent.com/jendrikseipp/rednotebook/b2cefe5f321b21ab7ad855059f3c0496eb0830d2/rednotebook/images/rednotebook-icon/rn-256.png" style="height:100px;max-width:100px;"><br>[RedNotebook](https://marph91.github.io/jimmy/formats/rednotebook/) | <img src="https://raw.githubusercontent.com/Automattic/simplenote-electron/4a140a96545763c849b26a81a2e27ff67eaa68f0/lib/icons/app-icon/icon_256x256.png" style="height:100px;max-width:100px;"><br>[Simplenote](https://marph91.github.io/jimmy/formats/simplenote/) | <img src="https://avatars.githubusercontent.com/u/24537496?s=100" style="height:100px;max-width:100px;"><br>[Standard&nbsp;Notes](https://marph91.github.io/jimmy/formats/standard_notes/) | <img src="https://www.synology.com/img/dsm/note_station/notestation_72.png" style="height:100px;max-width:100px;"><br>[Synology Note&nbsp;Station](https://marph91.github.io/jimmy/formats/synology_note_station/) |
Expand Down
9 changes: 6 additions & 3 deletions src/formats/tiddlywiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ def split_tags(tag_string: str) -> list[str]:
class Converter(converter.BaseConverter):
accepted_extensions = [".json"]

def convert(self, file_or_folder: Path):
resource_folder = common.get_temp_folder()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# we need a resource folder to avoid writing files to the source folder
self.resource_folder = common.get_temp_folder()

def convert(self, file_or_folder: Path):
file_dict = json.loads(Path(file_or_folder).read_text(encoding="utf-8"))
for note_tiddlywiki in file_dict:
title = note_tiddlywiki["title"]
Expand All @@ -73,7 +76,7 @@ def convert(self, file_or_folder: Path):
# Use the original filename if possible.
# TODO: Files with same name are replaced.
resource_title = note_tiddlywiki.get("alt-text")
temp_filename = resource_folder / (
temp_filename = self.resource_folder / (
common.unique_title()
if resource_title is None
else resource_title
Expand Down
131 changes: 131 additions & 0 deletions src/formats/turtl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Convert Turtl notes to the intermediate format."""

import base64
from pathlib import Path
import json

import common
import converter
import intermediate_format as imf
import markdown_lib.common


class Converter(converter.BaseConverter):
accepted_extensions = [".json"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# we need a resource folder to avoid writing files to the source folder
self.resource_folder = common.get_temp_folder()

def find_parent_notebook(self, space_id, board_id):
# first level: space
space = None
for notebook in self.root_notebook.child_notebooks:
if notebook.original_id == space_id:
space = notebook
break
if space is None:
self.logger.debug(f"Couldn't find space with id {space_id}")
return self.root_notebook

# second level: board
if board_id is None:
return space
for board in space.child_notebooks:
if board.original_id == board_id:
return board
self.logger.debug(f"Couldn't find board with id {board_id}")
return self.root_notebook

def handle_markdown_links(
self, body: str, path: Path
) -> tuple[imf.Resources, imf.NoteLinks]:
# pylint: disable=duplicate-code
# TODO
note_links = []
resources = []
for link in markdown_lib.common.get_markdown_links(body):
if link.is_web_link or link.is_mail_link:
continue # keep the original links
resource_path = path / link.url
if resource_path.is_file():
if common.is_image(resource_path):
# resource
resources.append(imf.Resource(resource_path, str(link), link.text))
else:
# TODO: this could be a resource, too. How to distinguish?
# internal link
note_links.append(
imf.NoteLink(str(link), Path(link.url).stem, link.text)
)
return resources, note_links

def convert(self, file_or_folder: Path):
file_dict = json.loads(file_or_folder.read_text(encoding="utf-8"))

for space in file_dict["spaces"]:
self.root_notebook.child_notebooks.append(
imf.Notebook(space["title"], original_id=space["id"])
)

for board in file_dict["boards"]:
for space in self.root_notebook.child_notebooks:
# TODO: Handle the error case when no space matches.
if space.original_id == board["space_id"]:
space.child_notebooks.append(
imf.Notebook(board["title"], original_id=board["id"])
)
break

file_map = {}
for file_ in file_dict["files"]:
# body seems to be empty always
file_map[file_["id"]] = file_["data"]

for note in file_dict["notes"]:
note_imf = imf.Note(
note["title"],
tags=[imf.Tag(t) for t in note["tags"]],
original_id=note["id"],
created=note["mod"],
)
match note["type"]:
case "file" | "image" | "link" | "text":
note_imf.body = note["text"]
case "password":
note_imf.body = "\n".join(
[
f"- Username: `{note["user_id"]}`",
f"- Password: `{note["password"]}`",
"",
note["text"],
]
)
case _:
self.logger.debug(f"Unhandled type \"{note["type"]}\"")
if note.get("url"):
note_imf.body += f"\n\n<{note["url"]}>"

# note["has_file"] seems to be sometimes wrong...
# I. e. if the type is "file". Check always.
# It seems like a note can have maximum one file attached.
if (file_data := file_map.get(note["id"])) is not None:
# TODO: files may be overwritten. use id?
filename = self.resource_folder / note["file"]["name"]
filename.write_bytes(base64.b64decode(file_data))
file_md = f"[{note["file"]["name"]}]({filename})"
note_imf.body += f"\n\n{file_md}"
# else:
# self.logger.debug(f"Couldn't find file with id {note["id"]}")

resources, note_links = self.handle_markdown_links(
note_imf.body, self.resource_folder
)
note_imf.resources = resources
note_imf.note_links = note_links

parent_notebook = self.find_parent_notebook(
note["space_id"], note["board_id"]
)
parent_notebook.child_notes.append(note_imf)
1 change: 1 addition & 0 deletions test/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def compare_dirs(dir1: Path, dir2: Path):
# [["tiddlywiki/test_1/tiddlers.json"]],
[["tomboy_ng/test_1/gnote"]],
[["tomboy_ng/test_2/tomboy-ng"]],
[["turtl/test_1/turtl-backup.json"]],
[["zettelkasten/test_1/test_zettelkasten.zkn3"]],
[["zim/test_1/notebook"]],
[["zoho_notebook/test_1/Notebook_14Apr2024_1300_html.zip"]],
Expand Down

0 comments on commit dc8360e

Please sign in to comment.