Post

Compiling Code Examples in Markdown Notes

I decided to beef up on C++ again. No particular reason, but it’s been a while and I’m still not quite confident with a number of additions made to the standard from C++11 onward. Maybe I’ll think of a project, or something, along the way but, in the meantime, I’ll pick up A Tour of C++ and have at it.

I like to make a lot of notes. Concepts in my own words, chapter summaries, links to various blogs and forums that add information and, most importantly, snippets of source code. Either source typed straight from the book, or with some embellishments and modifications so I can answer such important, ground-breaking questions like, What will it do if I change this bit?.

Markdown is fantastic for this. It’s easy to write a decent set of notes without all the faff and clicks of formatting in a modern word-processor. The best things is code blocks can be typeset in fixed-width and syntax-highlighted, but seamlessly and easily merged with the rest of the document.

Problem is, what if I want to compile and execute the code snippets? I have to copy them into a separate source file, first, and compile that. What if the snippet has syntax errors? I have to correct the source file then remember to copy the modified code back into the notes document.

So, I write a small Python script, and use my own tag in my Markdown notes. With the help of a Makefile I can embed code snippets and compile them straight from the notes document.

The tag I use is: CODE: <source_file_name>.cpp and I wrap it in comments so it doesn’t print out when I convert the file to PDF.

The Python script, below, will scan my markdown document for the tag <!-- CODE:<source_file_name>.cpp. Immediately below this tag is a standard markdown code-block, denoted by three ticks (```). This block will be extacted and written to a specified --out folder, then it’s ready to be compiled.

Here is the Python script I currently use - I called it xtract_code.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import argparse

CODE_SNIPPET_TOKEN='<!-- CODE:'

def parse_args():
    ap = argparse.ArgumentParser()
    ap.add_argument('--filename', required=True, help='Name of Markdown file containing code snippet.', type=str)
    ap.add_argument('--snippet', required=True, help=f'Name of code snippet in file and subsequent name of the extracted code file (denoted by {CODE_SNIPPET_TOKEN})', type=str)
    ap.add_argument('--outdir', required=False, help=f'Name of directory to write output file (defaults to current)', type=str)
    return ap.parse_args()

def get_code_file_name(line):
    return line[len(CODE_SNIPPET_TOKEN):].split(' ')[1].lstrip().rstrip()

def write_code_file(filename, code):
    with open(filename, 'w') as code_file:
        code_file.write(code)

def extract_code_snippet(filename, code_snippet):
    extracting_code = False
    code = ''
    with open(filename, 'r') as md_file:
        for line in md_file:
            if not extracting_code:
                if line.find(f'{CODE_SNIPPET_TOKEN}') >= 0:
                    code_snippet_filename = get_code_file_name(line)
                    if code_snippet_filename == code_snippet:
                        extracting_code = True
                        next(md_file)   
            else:
                if line.find('```') >= 0:
                    break
                code += line
    return code

def generate_code_file(input_filename, code_filename, code_dir):
    code = extract_code_snippet(input_filename, code_filename)
    write_code_file((f'{code_dir}/{code_filename}' if code_dir is not None else code_filename), code)

def main():
    args = parse_args()
    generate_code_file(args.filename, args.snippet, args.outdir)

if __name__ == '__main__':
    main()

Let’s say I have the following notes in a file called MyNotes.md.

1
2
3
4
5
6
7
8
9
10
## Chapter 1: Hello World

The example, below, prints 'Hello World!' to the screen, followed by a newline.

<!-- HelloWorld.cpp -->
```c++
int main() {
    cout << "Hello World!" << endl;
}
```

To call it, I just need to pass three parameters:

➜ python xtract_code.py --filename=./MyNotes.md --snippet=HelloWorld.cpp --outdir=./out

The script will look for the tag: <!-- CODE: HelloWorld.cpp --> in the MyNotes.md file and extract the following code-block to a source file called HelloWorld.cpp. You can now compile the source file using gcc, or your favourite compiler.

…But we can go a bit further with just a small Makefile.

1
2
3
4
5
6
7
compile-cpp: OUTFILE = `echo "${snippet}" | grep -Po '.*(?=\.)'` 
compile-cpp:
	python ./tools/xtract_code.py --filename=${notes} --snippet=${snippet} --outdir=./out
	g++ ./out/${snippet} -Wuninitialized -o ./out/${OUTFILE}

clean:
	rm -rf ./out/*    

Above, the compile-cpp target will extract and compile the code snippet, ready to be run. All we need to do is pass it the name of the Notes file and the code snippet

1
2
3
➜ make compile-cpp notes=./MyNotes.md snippet=HelloWorld.cpp
python ./tools/xtract_code.py --filename=./MyNotes.md --snippet=HelloWorld.cpp --outdir=./out
g++ ./out/HelloWorld.cpp -Wuninitialized -o ./out/`echo "HelloWorld.cpp" | grep -Po '.*(?=\.)'` 

Now I can stick all the code examples I want into my Markdown notes and compile them directly from the document, saving on the copying and pasting and repasting when they inevitably need correcting.

This post is licensed under CC BY 4.0 by the author.