Zettelkasten compatibility with Markdown apps

It turns out I’m too dumb to fully grok the tinderbox export mechanisms. But this prompted me to follow through on the promise of tinderbox not locking you in via having parsable xml files. Turns out the file format is quite beautiful and parsable! I thus created some code that takes all my tinderbox notes and spits out directories of markdown files, replete with wikilinks. I did this to play with noteplan3 a bit, but the files are equally readable (and links work) in obsidian and devonthink. For the sake of anyone who wants to do something similar I’ve added my code below.

A few notes:

  • this only takes the plain text part of each note, ignores any rtf formatting
  • it assumes that the link text and target id name are the same thing (which was true for me due to creating all links via zip links; would be quite easy to move away from that assumption).
  • assumes that interesting items can be selected via their prototypes.

The code is in R - would be just as easy in python or ruby or whatever, but I went with what I’ve used more recently. Needs the tidyverse, xml2, and stringi packages.

Really not sure this is useful to anyone, but just in case, here it is:

library(xml2)
library(tidyverse)
library(stringi)

### function definitions

add_wikilinks <- function(ID, text, links_df) {
  # find relevant links
  links <- links_df %>% filter(sstart>0 & sourceid==ID) %>% arrange(sstart)
  # return existing text if no links were found
  if (nrow(links)==0) {
    return(text)
  }
  
  # create substring: takes the link location and surrounds it by [[]]
  # this assumes that the source name and link text are the same.
  # then returns the modified text string
  wikified <- paste0('[[',
                     stri_sub(text, from=links$sstart+1, length=links$slen),
                     ']]')
  return(stri_sub_replace_all(text,
                              from=links$sstart+1,
                              length=links$slen,
                              value=wikified))
}

tbx_links <- function(tbx_xml) {
  # grab all link entries
  links <- xml_find_all(tbx_xml, ".//link")
  # convert link endtries to data frame, keeping link name sourceid, destid, and start and length
  links_df <- data.frame(
    name=links %>% xml_attr("name"),
    sourceid=links %>% xml_attr("sourceid"),
    destid=links %>% xml_attr("destid"),
    sstart=links %>% xml_attr("sstart") %>% as.numeric(),
    slen=links %>% xml_attr("slen") %>% as.numeric()
  )
  return(links_df)
}

tbx_items <- function(tbx_xml, links_df) {
  
  # grab all item entries
  items <- xml_find_all(tbx_xml, ".//item")
  
  # convert to dataframe
  items_df <- data.frame(
    # get item ID
    ID=items %>% xml_attr("ID"), 
    # get item prototype
    proto=items %>% xml_attr("proto"), 
    # find first Name attribute nested below this item
    Name=items %>% xml_find_first(".//attribute[@name='Name']") %>% xml_text(),
    # find first text entry nested below this item
    text=items %>% xml_find_first(".//text") %>% xml_text()
  ) %>% # now add the wikilinks and header text
    mutate(wikilinks=map2_chr(ID, text, add_wikilinks, links_df=links_df),
           wikitext=paste0('# ', Name, '\n\n', wikilinks))
  return(items_df)
}

write_to_md <- function(items_df, outpath, prototypes) {
  # check output directories, create if necessary
  dirs <- paste(outpath, prototypes, sep="/")
  for (d in dirs) {
    if (!dir.exists(d)) {
      dir.create(d, recursive = TRUE)
    }
  }
  
  # take the items
  items_df %>% 
    # keep just the one with the prespecified prototypes
    filter(proto %in% prototypes) %>% 
    # and write each to file
    split(.$ID) %>% 
    map(~ write_file(.x$wikitext, file=paste0(outpath, "/", .x$proto, "/", .x$Name, ".md"), append = FALSE))
}

### Run it for my data

# start by reading my tbx file
litnotes <- read_xml("~/Dropbox/litnotes.tbx")
# create a dataframe of all the links
links_df <- tbx_links(litnotes)
# create a dataframe of all the items; this includes wikilinkifying
items_df <- tbx_items(litnotes, links_df)
# write out those items with Zettel and Reference prototypes
write_to_md(items_df, "testout", c("Zettel", "Reference"))
1 Like