print("Hello world")
Table of Contents and Cross-Referencing
Conversion from code to documentation
2 Convert the code blocks into collapsibles
3 Convert the code to a html doc
pip install mindoc
mindoc example.py
"""
##### This is a heading
This is some text
"""
print("Hello world")
This is some text
print("Hello world")
mindoc example.sql
/*
##### This is a heading
This is some text
*/
SELECT *
FROM some_table
WHERE this_thing = that_thing
This is some text
SELECT *
FROM some_table
WHERE this_thing = that_thing
```mermaid
graph LR
A-->B
A-->C
B-->D
C-->D
```
graph LR
A-->B
A-->C
B-->D
C-->D
import os
import sys
import glob
import re
import itertools
import argparse
import subprocess
import time
import mistune
from bs4 import BeautifulSoup
def get_code(code_file_path) -> str:
code_file = open(code_file_path, 'r')
code = code_file.read()
code_file.close()
return code
def convert_python_blocks(code: str) -> str:
"""
Q: What happens to fenced docstrings that are meant for functions?
A: They remain within the code blocks.
So you can continue to use docstrings to document the functions if you want to.
"""
# Windows newline fix
windows_newline = u'\r'+'\n'
if windows_newline in code:
code = code.replace(windows_newline, '\n')
# Make all code blocks (which are between markdown blocks) collapsible
# Remove first fenced triplet of double quotes (ftdq)
first_ftdq = '"""' + '\n'
pre_html = code.replace(first_ftdq, '', 1)
# all subsequent ftdq must begin without any indentation and must end with a new line.
ftdq = '\n' + '"""' + '\n'
br = tag('br')
div = tag('div')
ediv = endtag('div')
collapsible_button = tag('button type="button" class="collapsible" style="width: 80px; text-align:center; margin-bottom:0px;"')
ebutton = endtag('button')
content_div = tag('div style=" margin: 0;" class="content"')
md_python_start = '\n```' + 'python\n\n'
md_python_end = u'```\n\n'
replace_with_pre = '\n' + br + br + collapsible_button + 'View code' + ebutton + content_div + md_python_start
replace_with_post = '\n' + md_python_end + ediv + '\n'
pre_html = replace_every_nth(pre_html, ftdq, replace_with_pre, nth=2)
pre_html = pre_html.replace(ftdq, replace_with_post)
pre_html = pre_html + replace_with_post
return pre_html
def convert_sql_blocks(code: str) -> str:
# Windows newline fix
windows_newline = u'\r'+'\n'
if windows_newline in code:
code = code.replace(windows_newline, '\n')
comment_start = '/' + '*'
comment_end = '*' + '/'
# Remove first comment block starter
pre_html = code.replace(comment_start, '', 1)
# Replace first comment block ender with a code block starter
br = tag('br')
div = tag('div')
ediv = endtag('div')
collapsible_button = tag('button type="button" class="collapsible" style="width: 80px; text-align:center; margin-bottom:0px;"')
ebutton = endtag('button')
content_div = tag('div style=" margin: 0;" class="content"')
md_sql_start = '\n```' + 'sql'
md_sql_end = u'```\n'
replace_with_pre = br + collapsible_button + 'View code' + ebutton + content_div + md_sql_start
replace_with_post = md_sql_end + ediv + '\n'
pre_html = pre_html.replace(comment_end, replace_with_pre, 1)
pre_html = pre_html + replace_with_post
return pre_html
def convert_to_html(pre_html: str) -> str:
meta = tag('meta name="viewport" content="width=device-width, initial-scale=1"')
style = tag('style') + u'''
body {
width: 90%; max-width: 1200px; margin: auto; font-family: Helvetica, arial, sans-serif; font-size: 14px; line-height: 1.6;
background-color: white; padding: 10px; color: #333;
}
/* CSS to make Markdown appear GitHub-style */
body > *:first-child { margin-top: 0 !important; }
body > *:last-child { margin-bottom: 0 !important; }
a { color: #4183C4; margin-top: 0; margin-bottom: 0; }
a.absent { color: #cc0000; }
a.anchor { display: block; padding-left: 30px; margin-left: -30px; cursor: pointer; position: absolute; top: 0; left: 0; bottom: 0; }
h1, h2, h3, h4, h5, h6 {
margin: 20px 0 5px; padding: 0; font-weight: bold; -webkit-font-smoothing: antialiased; cursor: text; position: relative;
}
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor {
background: no-repeat 10px center; text-decoration: none;
}
h1 tt, h1 code { font-size: inherit; }
h2 tt, h2 code { font-size: inherit; }
h3 tt, h3 code { font-size: inherit; }
h4 tt, h4 code { font-size: inherit; }
h5 tt, h5 code { font-size: inherit; }
h6 tt, h6 code { font-size: inherit; }
h1 { font-size: 28px; color: black; }
h2 { font-size: 24px; border-bottom: 1px solid #cccccc; color: black; }
h3 { font-size: 18px; }
h4 { font-size: 16px; }
h5 { font-size: 14px; }
h6 { color: #777777; font-size: 14px; }
p, blockquote, ul, ol, dl, li, table, pre { margin: 10px 0; }
hr { background: transparent repeat-x 0 0; border: 0 none; color: #cccccc; height: 4px; padding: 0; }
body > h2:first-child { margin-top: 0; padding-top: 0; }
body > h1:first-child { margin-top: 0; padding-top: 0; }
body > h1:first-child + h2 { margin-top: 0; padding-top: 0; }
body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child { margin-top: 0; padding-top: 0; }
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 { margin-top: 0; padding-top: 0; }
h1 p, h2 p, h3 p, h4 p, h5 p, h6 p { margin-top: 0; }
li p.first { display: inline-block; }
ul, ol { padding-left: 30px; }
li { margin: 2px; }
ul :first-child, ol :first-child { margin-top: 0; }
ul :last-child, ol :last-child { margin-bottom: 0; }
dl { padding: 0; }
dl dt { font-size: 14px; font-weight: bold; font-style: italic; padding: 0; margin: 15px 0 5px; }
dl dt:first-child { padding: 0; }
dl dt > :first-child { margin-top: 0; }
dl dt > :last-child { margin-bottom: 0; }
dl dd { margin: 0 0 15px; padding: 0 15px; }
dl dd > :first-child { margin-top: 0; }
dl dd > :last-child { margin-bottom: 0; }
blockquote { border-left: 4px solid #dddddd; padding: 0 15px; color: #777777; }
blockquote > :first-child { margin-top: 0; }
blockquote > :last-child { margin-bottom: 0; }
table { padding: 0; }
table tr { border-top: 1px solid #cccccc; background-color: white; margin: 0; padding: 0; }
table tr:nth-child(2n) { background-color: #f8f8f8; }
table tr th { font-weight: bold; border: 1px solid #cccccc; text-align: left; margin: 0; padding: 6px 13px; }
table tr td { border: 1px solid #cccccc; text-align: left; margin: 0; padding: 6px 13px; }
table tr th :first-child, table tr td :first-child { margin-top: 0; }
table tr th :last-child, table tr td :last-child { margin-bottom: 0; }
img { max-width: 100%; }
span.frame { display: block; overflow: hidden; }
span.frame > span { border: 1px solid #dddddd; display: block; float: left; overflow: hidden; margin: 13px 0 0; padding: 7px; width: auto; }
span.frame span img { display: block; float: left; }
span.frame span span { clear: both; color: #333333; display: block; padding: 5px 0 0; }
span.align-center { display: block; overflow: hidden; clear: both; }
span.align-center > span { display: block; overflow: hidden; margin: 13px auto 0; text-align: center; }
span.align-center span img { margin: 0 auto; text-align: center; }
span.align-right { display: block; overflow: hidden; clear: both; }
span.align-right > span { display: block; overflow: hidden; margin: 13px 0 0; text-align: right; }
span.align-right span img { margin: 0; text-align: right; }
span.float-left { display: block; margin-right: 13px; overflow: hidden; float: left; }
span.float-left span { margin: 13px 0 0; }
span.float-right { display: block; margin-left: 13px; overflow: hidden; float: right; }
span.float-right > span { display: block; overflow: hidden; margin: 13px auto 0; text-align: right; }
/* CSS that allows the collapsible code blocks */
.collapsible {
background-color: #ccc; padding: 5px; margin: 0; border: none; outline: none;
text-align: left; color: white; font-size: 12px;
cursor: pointer; width: 100%; }
.active, .collapsible:hover { background-color: #aaa; margin: 0; }
.content { margin: 0; background-color: transparent; padding: 0; max-height: 0; overflow: hidden; transition: max-height 0.15s ease-out; }
/* Code Prettify styling for the code blocks */
pre, code { margin: 0; padding: 0; }
pre code, pre tt { border: none; margin: 0; padding: 10px; }
pre .prettyprint { display: block; background-color: #333; margin: 0; }
pre .nocode { background-color: none; color: #000 }
pre .str { color: #ffa0a0 } /* string */
pre .kwd { color: #f0e68c; font-weight: bold } /* keyword */
pre .com { color: #87ceeb } /* comment */
pre .typ { color: #98fb98 } /* type */
pre .lit { color: #cd5c5c } /* literal */
pre .pun { color: #fff } /* punctuation */
pre .pln { color: #fff } /* plaintext */
pre .tag { color: #f0e68c; font-weight: bold } /* html/xml tag */
pre .atn { color: #bdb76b; font-weight: bold } /* attribute name */
pre .atv { color: #ffa0a0 } /* attribute value */
pre .dec { color: #98fb98 } /* decimal */
/* convert to light theme for printing */
@media print {
pre code, pre tt { background-color: none }
pre.prettyprint { background-color: none }
pre .str, code .str { color: #060 }
pre .kwd, code .kwd { color: #006; font-weight: bold }
pre .com, code .com { color: #600; font-style: italic }
pre .typ, code .typ { color: #404; font-weight: bold }
pre .lit, code .lit { color: #044 }
pre .pun, code .pun { color: #440 }
pre .pln, code .pln { color: #000 }
pre .tag, code .tag { color: #006; font-weight: bold }
pre .atn, code .atn { color: #404 }
pre .atv, code .atv { color: #060 }
}
''' + endtag('style')
# Make code collapsible
script = tag('script') + u'''
var coll = document.getElementsByClassName("collapsible");
var i;
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function() {
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.maxHeight){
content.style.maxHeight = null;
} else {
content.style.maxHeight = content.scrollHeight + "px";
}
});
}
''' + endtag('script')
# JavaScript styling of the code blocks
script += tag('script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"') + endtag('script')
script += tag('script src="https://cdnjs.cloudflare.com/ajax/libs/prettify/r298/lang-sql.min.js"') + endtag('script')
# JavaScript to allow MathJax
script += tag('script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"') + endtag('script')
script += tag('script type="text/x-mathjax-config"') + u'''
MathJax.Hub.Config({
tex2jax: {
inlineMath: [ ["$","$"], ["\\\\(","\\\\)"] ],
displayMath: [ ["$$",'$$'], ["\\\\[","\\\\]"] ],
processEscapes: true,
processEnvironments: true
},
// Center justify equations in code and markdown cells. Elsewhere
// we use CSS to left justify single line equations in code cells.
displayAlign: 'center',
"HTML-CSS": {
styles: {'.MathJax_Display': {"margin": 0}},
linebreaks: { automatic: true }
}
});
''' + endtag('script')
# JavaScript to allow MermaidJS (8.4.5 modified to neutral theme) for diagrams
script += tag('script src="https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js"') + endtag('script')
script += tag('script') + "var config = { startOnLoad:true }; mermaid.initialize(config);" + endtag('script')
# Convert the body markdown to html
renderer = mistune.Renderer(escape=True, hard_wrap=True, use_xhtml=False)
markdown = mistune.Markdown(renderer=renderer)
body = markdown(pre_html)
# Clean up some weird stuff that the markdown to html conversion introduced
br = tag('br')
body = body.replace(tag('p'), '')
body = body.replace(endtag('p'), br)
pre = tag('pre')
body = re.sub(br+r'[\w\W+]'+pre, pre, body)
# This bit allows the Google Code Prettify to work
body = body.replace('code ' + 'class="', 'code class="prettyprint ')
# This bit allows the MermaidJS to work
body = body.replace('prettyprint ' + 'lang-mermaid', 'mermaid')
# Put the html together
html = tag('!DOCTYPE html') + tag('html') + tag('head') + meta + style + endtag('head') + tag('body') + body + script + endtag('body') + endtag('html')
html = unescape(html)
return html
[ .py Code style ] (without the extra spaces)will become this --> .py Code style (yes, this is a link).
def create_toc(html: str) -> str:
soup = BeautifulSoup(html, "html.parser")
toc_html = tag('h3 style="color: #555" id="toc"')+'Table of Contents'+endtag('h3')
header_list = []
skip_first = 1
tag_number = 1
for header in soup.findAll(['h1', 'h2', 'h3', 'h4']):
header_string = header.string.replace('\n','').replace('\t','').strip().replace(' ','_').replace('.','_').lower()
header['id'] = header_string
header_list.append((header.string, header['id']))
if header.name=='h1':
indent = 'margin-left: 0px;'
elif header.name=='h2':
indent = 'margin-left: 20px;'
elif header.name=='h3':
indent = 'margin-left: 40px;'
elif header.name=='h4':
indent = 'margin-left: 60px;'
# link back to toc
if tag_number > skip_first:
new_tag = soup.new_tag("a")
new_tag.attrs['style'] = "font-size: 10px; color: #555;"
new_tag.attrs['href'] = "#toc"
new_tag.append("TOC")
br_tag = soup.new_tag("br")
header.insert_after(br_tag)
header.insert_after(new_tag)
toc_html = toc_html + tag('p style="margin-top:0px; margin-bottom: 0px; '+indent+'"') + tag('a style="color: #333; " '+f'''href="#{header['id']}"''') + header.string + endtag('a') + endtag('p') +'\n'
tag_number += 1
toc_html = toc_html + tag('br') + '\n'
toc_tag = '[TOC]'
html = soup.prettify(formatter="html5").replace(toc_tag, toc_html, 1)
# generate cross-reference links to headers
for header in header_list:
cross_ref_tag = '['+header[0].replace('\n','').replace('\t','').strip()+']'
cross_ref_html = tag('a style="color: #555; text-decoration: none;" '+f'''href="#{header[1]}"''') + header[0] + endtag('a')
html = html.replace(cross_ref_tag, cross_ref_html)
return html
def tag(element_name: str) -> str:
return u'<'+element_name + u'>'
def endtag(element_name: str) -> str:
return u'<' + '/' + element_name + u'>'
def replace_every_nth(original_string: str, substring_to_replace: str, replace_with: str, nth: int) -> str:
new_string = re.sub(f'({substring_to_replace})',
lambda m,
c = itertools.count(): m.group() if next(c) % nth else replace_with, original_string)
return new_string
def unescape(escaped: str) -> str:
unescaped = escaped.replace("<", u"<")
unescaped = unescaped.replace(">", u">")
unescaped = unescaped.replace("&", "&")
return unescaped
def save_as(content, file_path):
if not os.path.exists(os.path.dirname(file_path)):
os.makedirs(os.path.dirname(file_path), exist_ok=True)
file = open(file_path, 'w+', encoding='utf-8')
file.write(content)
file.close()
mindoc ./src/*.pyIf you are continuing to write the documentation and would like the changes to be continuously reflected in the .html file, use the watch flag (-w).
mindoc -w example.py
./awesome.py -> ./docs/awesome.htmlUnless the code file is in the src folder, then the documentation will be saved in the equivalent docs folder.
./awesome.md -> ./awesome.html
./src/awesome.py -> ./docs/awesome.html
def make_docs(code_files: list, print_production: bool):
for code_file_path in code_files:
code = get_code(code_file_path)
if code_file_path.endswith('.py'):
pre_html = convert_python_blocks(code)
elif code_file_path.endswith('.sql'):
pre_html = convert_sql_blocks(code)
elif code_file_path.endswith('.md'):
pre_html = code
else:
print('File type not supported')
pre_html = ''
html = convert_to_html(pre_html)
toc_tag = '[TOC]'
if toc_tag in html:
html = create_toc(html)
(dir_path, file_name) = os.path.split(code_file_path)
if file_name.endswith('.md'):
if dir_path == '':
dir_path = '.'
doc = '/'
elif dir_path == '':
doc = './docs/'
elif dir_path.endswith('src'):
dir_path = dir_path[:-3]+'docs/'
doc = ''
else:
doc = '/docs/'
html_file_path = dir_path + doc + file_name.replace('.py', '.html').replace('.sql', '.html').replace('.md', '.html')
save_as(html, html_file_path)
if print_production:
print(f'Doc for {code_file_path} saved as {html_file_path}.')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-w', '--watch', action='store_true', help='Watch original files and re-generate documentation on changes')
parser.add_argument("src_path", metavar="path", type=str, help="Path to code files to be converted to .html doc; accepts * as wildcard")
args = parser.parse_args()
print('')
files = glob.glob(args.src_path)
code_files = [x for x in files if x.endswith('.py')]
code_files += [x for x in files if x.endswith('.sql')]
code_files += [x for x in files if x.endswith('.md')]
make_docs(code_files, print_production=True)
if args.watch:
print('Watching...')
print('Ctrl+c to exit')
while True:
make_docs(code_files, print_production=False)
time.sleep(3)
print('')
if __name__ == "__main__":
main()