I don't know programing...I've just had the basics a long, long time ago...But with the help of ChatGpt I've got a Python Script to migrate from Joplin and It has worked just fine...Until the IA has solved the issue it has made a lot of mistakes , as usual.
I hope it helps on some way...
Here the link for the chat (mostly in Brazilian Portuguese) : https://chatgpt.com/share/69f593c2-b488-83e9-8dfa-e466701c4851
The script :
"""
Joplin → HelixNotes Migration Script (Generic Version)
DESCRIPTION
-----------
Converts a Joplin "Markdown + Front Matter" export into a structure
compatible with HelixNotes.
FEATURES
--------
- Preserves folder structure (notebooks → folders)
- Converts tags to normalized format
- Fixes note titles (Windows-safe filenames)
- Copies attachments to HelixNotes internal folder
- Rewrites image links to absolute paths (Helix-compatible)
- Converts Joplin internal links to wiki-style [[links]]
REQUIREMENTS
------------
- Python 3.8+
- Joplin export in: "Markdown + Front Matter"
HOW TO USE
----------
1. Export your notes from Joplin:
File → Export All → Markdown + Front Matter
2. Edit the paths below:
SRC = Path(r"YOUR_JOPLIN_EXPORT_FOLDER")
DST = Path(r"YOUR_HELIX_VAULT_FOLDER")
Example:
SRC = Path(r"C:\Users\YourName\Desktop\JoplinExport")
DST = Path(r"C:\Users\YourName\Documents\HelixVault")
3. Run:
python script.py
4. Open HelixNotes and select the DST folder as your vault.
NOTES
-----
- This script assumes HelixNotes stores attachments in:
.helixnotes/attachments/
- Images are converted to absolute paths because HelixNotes
may not render relative paths correctly.
- If images do not render, try removing "|size=small" in image links.
"""
import re
import shutil
from pathlib import Path
# ===== CONFIGURE HERE =====
SRC = Path(r"C:\PATH\TO\JOPLIN_EXPORT")
DST = Path(r"C:\PATH\TO\HELIX_VAULT")
ASSETS = DST / ".helixnotes" / "attachments"
DST.mkdir(parents=True, exist_ok=True)
ASSETS.mkdir(parents=True, exist_ok=True)
def slug_tag(tag):
tag = tag.strip().lower()
tag = re.sub(r"\s+", "-", tag)
return tag
def safe_name(name):
name = re.sub(r'[\\/:*?"<>|]', "-", name)
return name.strip()
def parse_frontmatter(text):
if not text.startswith("---"):
return {}, text
parts = text.split("---", 2)
if len(parts) < 3:
return {}, text
raw = parts[1]
body = parts[2].lstrip()
meta = {}
key = None
for line in raw.splitlines():
if re.match(r"^\s*-\s+", line) and key:
meta.setdefault(key, [])
meta[key].append(re.sub(r"^\s*-\s+", "", line).strip())
elif ":" in line:
k, v = line.split(":", 1)
key = k.strip()
v = v.strip()
meta[key] = v if v else []
return meta, body
def build_frontmatter(meta):
out = ["---"]
for k in ["title", "created", "updated"]:
if meta.get(k):
out.append(f"{k}: {meta[k]}")
tags = meta.get("tags", [])
if isinstance(tags, str):
tags = [tags]
out.append("tags:")
for t in sorted(set(slug_tag(x) for x in tags if x.strip())):
out.append(f" - {t}")
out.append("---")
out.append("")
return "\n".join(out)
def copy_resources():
res = SRC / "_resources"
if not res.exists():
print("No _resources folder found.")
return
for f in res.iterdir():
if f.is_file():
shutil.copy2(f, ASSETS / f.name)
def convert_image_links(txt):
def repl(m):
fname = m.group(1)
full = str(ASSETS / fname).replace("\\", "/")
return f""
txt = re.sub(
r'!\[\]\((?:.*?/)?_resources/([^)]+)\)',
repl,
txt
)
txt = re.sub(
r'!\[[^\]]*\]\((?:.*?/)?_resources/([^)]+)\)',
repl,
txt
)
return txt
def convert_note_links(txt):
txt = re.sub(
r'\[([^\]]+)\]\(:/[a-f0-9]{32}\)',
r'[[1円]]',
txt,
flags=re.I
)
return txt
def process_md(mdfile):
rel = mdfile.relative_to(SRC)
txt = mdfile.read_text(encoding="utf-8", errors="ignore")
meta, body = parse_frontmatter(txt)
title = safe_name(meta.get("title", mdfile.stem))
meta["title"] = title
body = convert_image_links(body)
body = convert_note_links(body)
final = build_frontmatter(meta) + body.strip() + "\n"
out_dir = DST / rel.parent
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / f"{title}.md").write_text(final, encoding="utf-8")
def main():
print("Copying attachments...")
copy_resources()
print("Processing notes...")
for md in SRC.rglob("*.md"):
if "_resources" not in str(md):
process_md(md)
print("DONE")
if __name__ == "__main__":
main()