Skip to content

Instantly share code, notes, and snippets.

@4k93n2
Last active January 27, 2024 23:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 4k93n2/b092ccd64ba549f0c5582dd212411bd7 to your computer and use it in GitHub Desktop.
Save 4k93n2/b092ccd64ba549f0c5582dd212411bd7 to your computer and use it in GitHub Desktop.
converts Dynalist's OPML so it can be imported into Workflowy
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()
@4k93n2
Copy link
Author

4k93n2 commented Jan 27, 2024

usage:

  • export a dynalist document as opml to the downloads folder
  • run this script
  • pick the .opml file you want to convert
  • copy the contents of the converted file to the clipboard and paste it into workflowy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment