I wrote a pretty basic script a long time ago that converts TaskPaper files to Markdown for nice previewing/publishing. I recently updated it with a bunch of modernizations.

I originally wrote this script (*goes to check*) — jebus, fifteen years ago. I guess I’ve been doing this stuff for a while now. Anyway, the original gist had require statements and processor flags that weren’t valid anymore, so I decided to fix that.

While I was in there, I added a few tweaks:

  • Use GFM-style - [ ] markers
  • Fix block quote indentation
  • convert @tags to #tags
  • bare URLs
  • Accept input piped on STDIN

The - [ ] markers mean that when rendered on GitHub (or in Marked with the right settings), the tasks will show up with checkboxes (checked if the line contains @done).

The ability to accept input on STDIN means you can include the script in a pipeline without having to read/write files. Not something I need to do often, but seemed like an obvious improvement.

The script is a bit kludgy — I added to it but didn’t bother rewriting it, so it reflects my Ruby skills 15 years ago. Maybe if I get bored on some downtime, I’ll take another stab at it, but it works, so I’m not inclined to put time into that right now. If you feel like rewriting it more elegantly, I’ll gladly publish your changes 😊.

The other updates are just niceties. Let me know if you have suggestions/questions, either in the Gist comments or on the forum!

tp2md.rb raw
#!/usr/bin/env ruby
# Usage: tp2md.rb filename.taskpaper > output.md
# Updated 2025-03-19
# - Fix block quote indentation
# - use GFM-style - [ ] markers
# - convert @tags to #tags
# - <self-link> bare urls
# - Accept input piped on STDIN

if $stdin.stat.size.positive?
  input = $stdin.read
  title = "TODO"
else
  input = File.read(ARGV[0])
  title = File.basename(ARGV[0],'.taskpaper').capitalize
end

output = "# #{title} #\n\n"
prevlevel = 0
begin
    input.each_line do |line|
      if line =~ /^(\t*)(.*?):(?:\s*)$/
        tabs = $1
        project = $2
        if tabs.nil?
          output += "\n## #{project} ##\n\n"
          prevlevel = 0
        else
          output += "#{tabs.gsub(/^\t/,'')}* **#{project}**\n"
          prevlevel = tabs.length
        end
      elsif line =~ /^(\t+)?\- (.*)$/
        task = $2
        tabs = $1.nil? ? '' : $1
        marker = task =~ /@done/ ? '- [x]' : '- [ ]'
        if tabs.length - prevlevel > 1
          tabs = "\t"
          prevlevel.times {|i| tabs += "\t"}
        end
        tabs = '' if prevlevel == 0 && tabs.length > 1
        output += "#{tabs.gsub(/^\t/,'')}#{marker} #{task.strip}\n"
        prevlevel = tabs.length
      else
        next if line =~ /^\s*$/
        tabs = ""
        prevlevel.times {|i| tabs += "\t"}
        output += "#{tabs}> #{line.strip}\n\n"
      end
    end
rescue => err
    puts "Exception: #{err}"
    err
end
output.gsub!(/(?mi)(?<!\]\(|\]: )\b((?:[\w-]+?:\/\/)(?:\S+?))(?=[\s\n)]|$)/, '<\1>')
puts output.gsub(/@(?=\S)/, '#')
Copy