Last active
January 27, 2024 23:37
-
-
Save 4k93n2/b092ccd64ba549f0c5582dd212411bd7 to your computer and use it in GitHub Desktop.
converts Dynalist's OPML so it can be imported into Workflowy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from tkinter import Tk, messagebox | |
from tkinter.filedialog import askopenfilename | |
from pathlib import Path | |
import xml.etree.ElementTree as ET | |
import re | |
from datetime import datetime | |
import html | |
color_list = ["red", "orange", "yellow", "green", "sky", "purple"] | |
# user settings: | |
convert_dynalist_urls = False # whether to convert [markdown](links) that contain dynalist urls | |
def convert_opml(opml_string): | |
"""converts Dynalist's OPML so it can be imported into Workflowy""" | |
root = ET.fromstring(opml_string) | |
for outline in root.iter("outline"): | |
# main text | |
text = outline.get("text") | |
if text is not None: | |
outline.set("text", convert_text(text)) | |
# note text | |
note = outline.get("_note") | |
if note is not None: | |
outline.set("_note", convert_text(note)) | |
# colour | |
color_num = outline.get("colorLabel") | |
if color_num is not None: | |
color = color_list[int(color_num) - 1] | |
text = outline.get("text") | |
outline.set("text", f"<span class='colored bc-{color}'>{text}</span>") | |
# complete | |
complete = outline.get("complete") | |
if complete is not None: | |
# create workflowy's "_complete" attribute | |
outline.attrib["_complete"] = outline.attrib["complete"] | |
del outline.attrib["complete"] # delete dynalist's "complete" attribute | |
opml_output = ET.tostring(root, encoding="utf8").decode("utf8") | |
return opml_output | |
def convert_text(string): | |
# bold text | |
if "**" in string: | |
string = convert_formatting(string, "**", "b") | |
# italic text | |
if "__" in string: | |
string = convert_formatting(string, "__", "i") | |
# dates | |
if "!(" in string and ")" in string: | |
string = convert_dates(string) | |
# markdown links | |
if "[" in string and "](" in string and ")" in string: | |
string = convert_markdown_links(string) | |
# dynalist's highlights to workflowy's yellow | |
if "==" in string: | |
yellow_class = "<span class='colored bc-yellow'>text</span>" | |
string = re.sub(r'==(.+?)==', lambda match: yellow_class.replace('text', match.group(1)), string) | |
return string | |
def convert_formatting(string, markdown, html_tag): | |
pattern = rf"{re.escape(markdown)}(.*?){re.escape(markdown)}" | |
output = re.sub(pattern, rf"<{html_tag}>\1</{html_tag}>", string) | |
return output | |
def convert_dates(string): | |
""" | |
converts dates and date ranges such as !(YYYY-MM-DD) to: | |
<time startYear='YYYY' startMonth='MM' startDay='DD'>Day, Month DD, YYYY</time> | |
""" | |
def repl(match): | |
# split the match into start and end dates | |
dates = [datetime.strptime(date, "%Y-%m-%d") for date in match.group(1).split(" - ")] | |
time_tag = "<time" | |
for i, date in enumerate(dates): | |
prefix = 'start' if i == 0 else 'end' # add start or end attributes depending on the index | |
time_tag += (f" {prefix}Year='{date.year}'" | |
f" {prefix}Month='{date.month}'" | |
f" {prefix}Day='{date.day}'") | |
time_tag += ">" # add date(s) in the format: Day, Month DD, YYYY | |
time_tag += " - ".join([f"{date.strftime('%a, %b')} {date.day}, {date.year}" for date in dates]) | |
time_tag += "</time>" | |
return time_tag | |
output = re.sub(r"!\((.*?)\)", repl, string) | |
return output | |
def convert_markdown_links(string): | |
def repl(match): | |
text = match.group(1) | |
url = match.group(2) | |
if url.startswith("https://dynalist.io/d/") and convert_dynalist_urls is False: | |
return match.group(0) # return original markdown link | |
else: | |
return f"<a href='{url}'>{text}</a>" | |
output = re.sub(r"\[(.*?)\]\((.*?)\)", repl, string) | |
return output | |
def pick_opml_file(): | |
# create a tkinter window | |
root = Tk() | |
root.withdraw() | |
# set defaults | |
default_folder = Path.home() / "downloads" | |
file_types = [("OPML files", "*.opml")] | |
# show file picker | |
selected = askopenfilename(initialdir=default_folder, filetypes=file_types) | |
root.destroy() # close tkinter | |
if selected: | |
return Path(selected) | |
def document_name(opml_string): | |
root = ET.fromstring(opml_string) | |
# get the first 'outline' element | |
first_outline = next(root.iter("outline"), None) | |
if first_outline is not None: | |
doc_name = first_outline.get('text') | |
return doc_name | |
def main(): | |
file = pick_opml_file() | |
if file: | |
with open(file, "r", encoding="utf-8") as f: | |
opml_string = f.read() | |
# convert | |
new_opml = convert_opml(opml_string) | |
# get document name | |
doc_name = document_name(opml_string) | |
# remove characters not allowed in a filename | |
doc_name = re.sub(r'[\\/:*?"<>|]', '', doc_name) | |
# create new file path | |
filename = file.stem.replace("dynalist", "workflowy") | |
new_file = file.parent / f"{filename}_{doc_name}.opml" | |
# save | |
with open(new_file, "w", encoding="utf-8") as f: | |
f.write(new_opml) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
usage: