Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Markdown Converter

Sometimes I write books (or try too, at least). I write in a single markdown file because I find this works for me.

This does, however, pose a problem. You’ve probably notied this website is a mdbook instance that I’ve used as a blog. Needless to say, I think mdbook is really cool.

Unfortunately, mdbook isn’t great for single markdown files – by which I mean it doesn’t work at all. I had a quick look on the web, but couldn’t find anything that would convert for me quick and easily. So I wrote my own! And in the spirit of free software, it is below. Whilst I’d prefer to use PyQt, I have aproject going on at my real work that needs tkinter so I used this as a little practice.

Any problems? Let me know at kay at kayjoseph dot xyz.

#!/usr/bin/env python
'''
Markdown to mdbook converter
Kay Saint Joseph
This program is licensed under the GNU General Public License v3
'''

from tkinter import *
from tkinter import ttk
from tkinter import filedialog
import sys
import os

## convenience function
def read_file(fname) -> list:
    with open(fname, 'r') as f:
        return f.readlines()

## removes comments from the readlines book. Comments are HTML "<!-- ... -->"
def remove_comments(f_lines) -> list:
    comment = False
    ret = []
    for line in f_lines:
        if "<!--" in line:
            comment = True
        if not comment:
            ret.append(line)
        if "-->" in line:
            comment = False
    return ret

# take lines of book. return list of mdbook style SUMMARY.md
def create_summary(lines: list) -> list:
    summary = []
    titles = filter(lambda x: x[0]=='#', lines)
    for title in titles:
        section = len(list(filter(lambda x: x=='#', title)))
        name = title.lstrip(' #').rstrip()
        md = get_chapter_md(title)
        if section == 1:
            summary.append("# {}".format(name))
        else:
            summary.append(" - [{}]({})".format(name, md))
    summary = list(map(lambda x: x+'\n', summary))
    return summary

## takes every line in book and returns dict {chapter name: [line 1, line 2]}
def create_chapters(lines: list) -> dict:
    ret_dict = {}
    chapter_title = ""
    chapter_text = []
    for line in lines:
        if line[0] == '#':
            if chapter_title != "":
                ret_dict[chapter_title] = chapter_text
            chapter_title = line
            chapter_text = []
        else:
            chapter_text.append(line)
    else:
        ret_dict[chapter_title] = chapter_text
    return ret_dict

## debug func
def printdict(d):
    for (k,v) in d.items():
        print(k+": "+str(v[0:3]))

## Take a chapter name such as "The Bog" and turn it into a md file name like
## "thebog.md"
def get_chapter_md(s: str) -> str:
    s = s.lstrip(' #').lower().replace(' ', '_')
    s = ''.join(filter(lambda x: x.isalpha() or x==' ', s))
    s = s + ".md"
    return s

## Add a front page for the mdbook
def write_preamble(s: str):
    with open("src/preamble.md", "w") as f:
        f.writelines(s)

## where the actual work is done
def convert_to_mdbook(book_name: str, author: str, main_md="./main.md", ):
    book = read_file(main_md)
    book = remove_comments(book)
    summary = create_summary(book)
    book = create_chapters(book)
    preamble = ['# ' + book_name + "\n\n", author + "\n"]
    write_preamble(preamble)


    with open("src/SUMMARY.md", "w") as f:
        f.write("[{}](preamble.md)\n".format(book_name))
        f.writelines(summary)
    for (k,v) in book.items():
        fname = "src/" + get_chapter_md(k)
        with open(fname, "w") as f:
            head = "<b>" + k.lstrip(' #') + "</b>\n\n"
            f.write(head)
            f.writelines(v)

## GUI
class App(Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack()

        ttk.Label(text="MD Booker").pack(pady=10)

        self.t_cont = StringVar()
        self.a_cont = StringVar()
        self.main_md_file = StringVar()

        ttk.Label(text="Title").pack(pady=10)
        self.title = Entry(textvariable=self.t_cont).pack(padx=10)
        ttk.Label(text="Author").pack()
        self.author = Entry(textvariable=self.a_cont).pack(padx=10)

        self.file_label = ttk.Label().pack(side="right")
        self.md_button = ttk.Button(
            text="Pick Markdown File", command=self.md_filepick
        ).pack(pady=10)

        ttk.Button(
            text="Run",
            command=self.process
        ).pack(side="left", pady=10)
        ttk.Button(text="Quit", command=kill_fail).pack(side="right", pady=10)

    def get_title(self):
        return self.t_cont.get()

    def get_author(self):
        return self.a_cont.get()

    ## take the GUI entered info and run the converter itself
    def process(self):
        t = self.get_title()
        a = self.get_author()
        f = self.main_md_file.get()

        if t != "" and a != "" and f != "":
            # print("Title: " + t)
            # print("Author: " + a)
            # print("Main Markdown File: " + f)
            convert_to_mdbook(t, a, main_md=f)
        else:
            raise ValueError("Boxes Empty")
            ttk.messagebox.showinfo("Error", "Boxes Empty")

        sys.exit(0)

    ## pick the main book markdown file to convert
    def md_filepick(self):
        i_dir = os.getcwd()
        filename = filedialog.askopenfilename(
            initialdir = i_dir,
            title = "Pick File"
        )
        self.main_md_file.set(filename)
        self.md_button.config(text=filename)
        return filename

## Option setter from entered args
class Opt():
    def __init__(self, args):
        self.is_gui = False

        if "-h" in args:
            raise UsageError
        elif "-g" in args:
            self.is_gui = True
        else:
            if len(args) != 7:
                raise SyntaxError("Error: too many arguments")
            for i in range(1, len(args)-1):
                if args[i] == "-a":
                    self.author = args[i+1]
                elif args[i] == "-t":
                    self.title = args[i+1]
                elif args[i] == "-f":
                    self.md_file = args[i+1]
                else:
                    continue
            # print("Title: " + self.title)
            # print("Author: " + self.author)
            # print("Markdown file: " + self.md_file)

class UsageError(Exception):
    pass

def kill_fail():
    sys.exit(1)

def usage():
    print(
'''\
MD Converter: a utility for turning a single markdown book into an mdbook
compliant setup.
Usage:  mdconverter -g
        mdconverter -a <author> -t <title> -f <single_markdown_file>
        mdconverter -h
'''
    )
    kill_fail()

def main(args):
    try:
        opt = Opt(args)
    except SyntaxError as e:
        print(e)
        usage()
    except UsageError:
        usage()

    if opt.is_gui:
        root = Tk()
        gui = App(root)
        gui.master.title("MD Book Converter")
        gui.mainloop()
    else:
        try:
            convert_to_mdbook(opt.title, opt.author, main_md=opt.md_file)
        except:
            usage()


if __name__ == "__main__":
    main(sys.argv)