Skip to content

Commit

Permalink
add partial support for zettelkasten
Browse files Browse the repository at this point in the history
  • Loading branch information
marph91 committed Sep 22, 2024
1 parent 2e861d5 commit 93fcc67
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 3 deletions.
1 change: 0 additions & 1 deletion docs/contributing/more_note_apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,4 @@ Loose collection of note apps/messengers/wikis/formats that could be implemented
| [Xiaomi Notes](https://i.mi.com/note/h5) | | account needed |
| [XWiki](https://www.xwiki.org/) | [doc](https://www.xwiki.org/xwiki/bin/view/Documentation/UserGuide/Features/Exports) | |
| [Zim](https://zim-wiki.org/index.html) | - [doc](https://zim-wiki.org/manual/Help/Export.html) (Markdown) <br>- [script](https://gist.github.com/reagle/7418f54fb6e40fe8d925e1c3f5325076) | |
| [Zettelkasten](http://zettelkasten.danielluedecke.de/) | [doc](https://github.com/Zettelkasten-Team/Zettelkasten/wiki/Exports) (Markdown) | |
| [Zotero](https://www.zotero.org/) | [doc](https://www.zotero.org/support/kb/exporting) | |
18 changes: 18 additions & 0 deletions docs/formats/zettelkasten.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
This page describes how to convert notes from Zettelkasten to Markdown.

## General Information

- [Website](http://zettelkasten.danielluedecke.de/)
- Typical extension: `.zkn3`

## Instructions

Zettelkasten supports Markdown export. However, it doesn't convert everything and the note links and attachments get lost. Most data can be preserved, when using the original `.zkn3` location in the filesystem without any export.

1. [Install jimmy](../index.md#installation)
2. Convert to Markdown. Example: `jimmy-cli-linux test_zettelkasten.zkn3 --format zettelkasten`
3. [Import to your app](../import_instructions.md)

## Known Limitations

The note body is still formatted in [BBCode](https://en.wikipedia.org/wiki/BBCode) until a good conversion path is available (see [github issue](https://github.com/marph91/jimmy/issues/14)).
2 changes: 1 addition & 1 deletion src/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def convert_file(self, file_: Path, parent: imf.Notebook):
case ".md" | ".markdown" | ".txt" | ".text":
note_body = file_.read_text(encoding="utf-8")
case ".fountain":
# Simply wrap in a code block. This is supported in
# Simply wrap in a code block. This is supported in
# Joplin and Obsidian via plugins.
note_body_fountain = file_.read_text(encoding="utf-8")
note_body = f"```fountain\n{note_body_fountain}\n```\n"
Expand Down
89 changes: 89 additions & 0 deletions src/formats/zettelkasten.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Convert Zettelkasten notes to the intermediate format."""

import datetime as dt
from pathlib import Path
import xml.etree.ElementTree as ET # noqa: N817

import common
import converter
import intermediate_format as imf


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

def prepare_input(self, input_: Path) -> Path:
return common.extract_zip(input_)

def parse_attributes(self, zettel, note_imf: imf.Note):
for key, value in zettel.attrib.items():
match key:
case "zknid":
note_imf.original_id = value
case "ts_created":
note_imf.created = dt.datetime.strptime(value, "%y%m%d%H%M")
case "ts_edited":
note_imf.updated = dt.datetime.strptime(value, "%y%m%d%H%M")
case "rating" | "ratingcount":
# TODO: add when arbitrary metadata is supported
pass
case _:
self.logger.warning(f"ignoring attribute {key}={value}")

def convert(self, file_or_folder: Path):
self.root_path = self.prepare_input(file_or_folder)

attachments_folder = file_or_folder.parent / "attachments"
attachments_available = attachments_folder.is_dir()
if not attachments_available:
self.logger.warning(
f"No attachments folder found at {attachments_folder}. "
"Attachments are not converted."
)

tag_id_name_map = {}
root_node = ET.parse(self.root_path / "keywordFile.xml").getroot()
for keyword in root_node.findall("entry"):
if (tag_id := keyword.attrib.get("f")) is not None:
tag_id_name_map[tag_id] = keyword.text

root_node = ET.parse(self.root_path / "zknFile.xml").getroot()
for zettel in root_node.findall("zettel"):
title = item.text if (item := zettel.find("title")) is not None else ""
assert title is not None
self.logger.debug(f'Converting note "{title}"')
note_imf = imf.Note(title)

self.parse_attributes(zettel, note_imf)

for item in zettel:
match item.tag:
case "title":
pass # handled already
case "content":
note_imf.body = item.text if item.text else ""
case "author":
note_imf.author = item.text
case "keywords":
if item.text is not None:
for tag_id in item.text.split(","):
tag_name = tag_id_name_map.get(tag_id, tag_id)
assert tag_name is not None
note_imf.tags.append(imf.Tag(tag_name))
case "links":
if not attachments_available:
continue
# links = resources are always attached at the end
for link in item.findall("link"):
if link.text is None:
continue
note_imf.resources.append(
imf.Resource(attachments_folder / link.text)
)
case "luhmann" | "misc" | "zettel":
pass # always None
case "manlinks":
pass # TODO: Should correspond to the parsed note links.
case _:
self.logger.warning(f"ignoring item {item.tag}={item.text}")
self.root_notebook.child_notes.append(note_imf)
3 changes: 2 additions & 1 deletion src/jimmy.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ def jimmy(config) -> common.Stats:
"[link=https://discourse.joplinapp.org/t/jimmy-a-joplin-import-tool/38503]"
"Joplin forum[/link], "
"[link=https://forum.obsidian.md/t/jimmy-convert-your-notes-to-markdown/88685]"
"Obsidian forum[/link] or an [link=mailto:martin.d@andix.de]email[/link].[/bold]"
"Obsidian forum[/link] or an [link=mailto:martin.d@andix.de]email[/link]."
"[/bold]"
)

return stats
1 change: 1 addition & 0 deletions test/manual_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def main():
[cache / "tiddlywiki/tiddlyhost.json", "--format" ,"tiddlywiki"],
[cache / "tomboy_ng/gnote/", "--format" ,"tomboy_ng"],
[cache / "tomboy_ng/tomboy-ng/", "--format" ,"tomboy_ng"],
[cache / "zettelkasten/example/test_zettelkasten.zkn3", "--format" ,"zettelkasten"],
[cache / "zim_md"],
[cache / "zoho_notebook/Notebook_14Apr2024_1300_html.zip", "--format" ,"zoho_notebook"],
]
Expand Down

0 comments on commit 93fcc67

Please sign in to comment.