Add my writing framework.
It's still VERY VERY MESSY and will probably stay that way for a long time aka forever.
This commit is contained in:
parent
0841204daa
commit
aed1ca2a3d
6 changed files with 1151 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
voussoir.net/writing/**/*.html
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
https://voussoir.net
|
197
voussoir.net/writing/css/dark.css
Normal file
197
voussoir.net/writing/css/dark.css
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
:root
|
||||||
|
{
|
||||||
|
--color_bodybg: #272822;
|
||||||
|
--color_codebg: rgba(255, 255, 255, 0.05);
|
||||||
|
--color_codeborder: rgba(255, 255, 255, 0.2);
|
||||||
|
--color_h1bg: #284142;
|
||||||
|
--color_htmlbg: #1b1c18;
|
||||||
|
--color_blockquotebg: rgba(0, 0, 0, 0.2);
|
||||||
|
--color_inlinecodebg: rgba(255, 255, 255, 0.1);
|
||||||
|
--color_link: #ae81ff;
|
||||||
|
--color_maintext: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
*
|
||||||
|
{
|
||||||
|
font-family: Verdana, sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
color: var(--color_maintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5
|
||||||
|
{
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
h2, h3, h4, h5
|
||||||
|
{
|
||||||
|
border-bottom: 1px solid var(--color_maintext);
|
||||||
|
/*background-color: var(--color_h1bg);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {font-size: 2.5em;} h1 * {font-size: inherit;}
|
||||||
|
h2 {font-size: 1.8em;} h2 * {font-size: inherit;}
|
||||||
|
h3 {font-size: 1.5em;} h3 * {font-size: inherit;}
|
||||||
|
h4 {font-size: 1.2em;} h4 * {font-size: inherit;}
|
||||||
|
h5 {font-size: 1.0em;} h5 * {font-size: inherit;}
|
||||||
|
|
||||||
|
|
||||||
|
.header_anchor_link {display: none; font-size: 1.0em; text-decoration: none}
|
||||||
|
h1:hover > .header_anchor_link {display: initial;}
|
||||||
|
h2:hover > .header_anchor_link {display: initial;}
|
||||||
|
h3:hover > .header_anchor_link {display: initial;}
|
||||||
|
h4:hover > .header_anchor_link {display: initial;}
|
||||||
|
h5:hover > .header_anchor_link {display: initial;}
|
||||||
|
|
||||||
|
|
||||||
|
html
|
||||||
|
{
|
||||||
|
padding-top: 30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
background-color: var(--color_htmlbg);
|
||||||
|
}
|
||||||
|
|
||||||
|
a
|
||||||
|
{
|
||||||
|
color: var(--color_link);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
body
|
||||||
|
{
|
||||||
|
width: 80%;
|
||||||
|
min-width: 30em;
|
||||||
|
max-width: 70em;
|
||||||
|
margin: auto;
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: 64px;
|
||||||
|
box-shadow: #000 0px 0px 40px -10px;
|
||||||
|
background-color: var(--color_bodybg);
|
||||||
|
}
|
||||||
|
|
||||||
|
body *
|
||||||
|
{
|
||||||
|
max-width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote
|
||||||
|
{
|
||||||
|
background-color: var(--color_blockquotebg);
|
||||||
|
margin-inline-start: 0;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
padding-inline-start: 40px;
|
||||||
|
padding-inline-end: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:not(pre) > code
|
||||||
|
{
|
||||||
|
background-color: var(--color_inlinecodebg);
|
||||||
|
border-radius: 3px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre
|
||||||
|
{
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--color_codeborder);
|
||||||
|
background-color: var(--color_codebg);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
pre,
|
||||||
|
.highlight *
|
||||||
|
{
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Thank yourichleland for pre-building this Monokai style.
|
||||||
|
https://github.com/richleland/pygments-css
|
||||||
|
*/
|
||||||
|
:root
|
||||||
|
{
|
||||||
|
--color_monokai_bg: #272822;
|
||||||
|
--color_monokai_purple: #ae81ff;
|
||||||
|
--color_monokai_green: #a6e22e;
|
||||||
|
--color_monokai_pink: #f92672;
|
||||||
|
--color_monokai_white: #f8f8f2;
|
||||||
|
--color_monokai_orange: #fd971f;
|
||||||
|
--color_monokai_yellow: #e6db74;
|
||||||
|
--color_monokai_blue: #66d9ef;
|
||||||
|
}
|
||||||
|
.highlight .hll { background-color: #49483e }
|
||||||
|
.highlight { background-color: var(--color_monokai_bg); color: var(--color_monokai_white) }
|
||||||
|
.highlight .c { color: #75715e } /* Comment */
|
||||||
|
.highlight .err { color: #960050; background-color: #1e0010 } /* Error */
|
||||||
|
.highlight .k { color: var(--color_monokai_pink) } /* Keyword */
|
||||||
|
.highlight .l { color: var(--color_monokai_purple) } /* Literal */
|
||||||
|
.highlight .n { color: var(--color_monokai_white) } /* Name */
|
||||||
|
.highlight .o { color: var(--color_monokai_pink) } /* Operator */
|
||||||
|
.highlight .p { color: var(--color_monokai_white) } /* Punctuation */
|
||||||
|
.highlight .ch { color: #75715e } /* Comment.Hashbang */
|
||||||
|
.highlight .cm { color: #75715e } /* Comment.Multiline */
|
||||||
|
.highlight .cp { color: #75715e } /* Comment.Preproc */
|
||||||
|
.highlight .cpf { color: #75715e } /* Comment.PreprocFile */
|
||||||
|
.highlight .c1 { color: #75715e } /* Comment.Single */
|
||||||
|
.highlight .cs { color: #75715e } /* Comment.Special */
|
||||||
|
.highlight .gd { color: var(--color_monokai_pink) } /* Generic.Deleted */
|
||||||
|
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||||
|
.highlight .gi { color: var(--color_monokai_green) } /* Generic.Inserted */
|
||||||
|
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||||
|
.highlight .gu { color: #75715e } /* Generic.Subheading */
|
||||||
|
.highlight .kc { color: var(--color_monokai_blue) } /* Keyword.Constant */
|
||||||
|
.highlight .kd { color: var(--color_monokai_blue) } /* Keyword.Declaration */
|
||||||
|
.highlight .kn { color: var(--color_monokai_pink) } /* Keyword.Namespace */
|
||||||
|
.highlight .kp { color: var(--color_monokai_blue) } /* Keyword.Pseudo */
|
||||||
|
.highlight .kr { color: var(--color_monokai_blue) } /* Keyword.Reserved */
|
||||||
|
.highlight .kt { color: var(--color_monokai_blue) } /* Keyword.Type */
|
||||||
|
.highlight .ld { color: var(--color_monokai_yellow) } /* Literal.Date */
|
||||||
|
.highlight .m { color: var(--color_monokai_purple) } /* Literal.Number */
|
||||||
|
.highlight .s { color: var(--color_monokai_yellow) } /* Literal.String */
|
||||||
|
.highlight .na { color: var(--color_monokai_white) } /* Name.Attribute */
|
||||||
|
.highlight .narg {color: var(--color_monokai_orange) } /* Custom Name.Argument */
|
||||||
|
.highlight .nb { color: var(--color_monokai_white) } /* Name.Builtin */
|
||||||
|
.highlight .nc { color: var(--color_monokai_white) } /* Name.Class */
|
||||||
|
.highlight .no { color: var(--color_monokai_blue) } /* Name.Constant */
|
||||||
|
.highlight .nd { color: var(--color_monokai_green) } /* Name.Decorator */
|
||||||
|
.highlight .ni { color: var(--color_monokai_white) } /* Name.Entity */
|
||||||
|
.highlight .ne { color: var(--color_monokai_blue) } /* Name.Exception */
|
||||||
|
.highlight .nf { color: var(--color_monokai_green) } /* Name.Function */
|
||||||
|
.highlight .nl { color: var(--color_monokai_white) } /* Name.Label */
|
||||||
|
.highlight .nn { color: var(--color_monokai_white) } /* Name.Namespace */
|
||||||
|
.highlight .nx { color: var(--color_monokai_green) } /* Name.Other */
|
||||||
|
.highlight .py { color: var(--color_monokai_white) } /* Name.Property */
|
||||||
|
.highlight .nt { color: var(--color_monokai_pink) } /* Name.Tag */
|
||||||
|
.highlight .nv { color: var(--color_monokai_white) } /* Name.Variable */
|
||||||
|
.highlight .ow { color: var(--color_monokai_pink) } /* Operator.Word */
|
||||||
|
.highlight .w { color: var(--color_monokai_white) } /* Text.Whitespace */
|
||||||
|
.highlight .mb { color: var(--color_monokai_purple) } /* Literal.Number.Bin */
|
||||||
|
.highlight .mf { color: var(--color_monokai_purple) } /* Literal.Number.Float */
|
||||||
|
.highlight .mh { color: var(--color_monokai_purple) } /* Literal.Number.Hex */
|
||||||
|
.highlight .mi { color: var(--color_monokai_purple) } /* Literal.Number.Integer */
|
||||||
|
.highlight .mo { color: var(--color_monokai_purple) } /* Literal.Number.Oct */
|
||||||
|
.highlight .sa { color: var(--color_monokai_white) } /* Literal.String.Affix */
|
||||||
|
.highlight .sb { color: var(--color_monokai_yellow) } /* Literal.String.Backtick */
|
||||||
|
.highlight .sc { color: var(--color_monokai_yellow) } /* Literal.String.Char */
|
||||||
|
.highlight .dl { color: var(--color_monokai_yellow) } /* Literal.String.Delimiter */
|
||||||
|
.highlight .sd { color: var(--color_monokai_yellow) } /* Literal.String.Doc */
|
||||||
|
.highlight .s2 { color: var(--color_monokai_yellow) } /* Literal.String.Double */
|
||||||
|
.highlight .se { color: var(--color_monokai_purple) } /* Literal.String.Escape */
|
||||||
|
.highlight .sh { color: var(--color_monokai_yellow) } /* Literal.String.Heredoc */
|
||||||
|
.highlight .si { color: var(--color_monokai_yellow) } /* Literal.String.Interpol */
|
||||||
|
.highlight .sx { color: var(--color_monokai_yellow) } /* Literal.String.Other */
|
||||||
|
.highlight .sr { color: var(--color_monokai_yellow) } /* Literal.String.Regex */
|
||||||
|
.highlight .s1 { color: var(--color_monokai_yellow) } /* Literal.String.Single */
|
||||||
|
.highlight .ss { color: var(--color_monokai_yellow) } /* Literal.String.Symbol */
|
||||||
|
.highlight .bp { color: var(--color_monokai_white) } /* Name.Builtin.Pseudo */
|
||||||
|
.highlight .fm { color: var(--color_monokai_blue) } /* Name.Function.Magic */
|
||||||
|
.highlight .vc { color: var(--color_monokai_white) } /* Name.Variable.Class */
|
||||||
|
.highlight .vg { color: var(--color_monokai_white) } /* Name.Variable.Global */
|
||||||
|
.highlight .vi { color: var(--color_monokai_white) } /* Name.Variable.Instance */
|
||||||
|
.highlight .vm { color: var(--color_monokai_white) } /* Name.Variable.Magic */
|
||||||
|
.highlight .il { color: var(--color_monokai_purple) } /* Literal.Number.Integer.Long */
|
282
voussoir.net/writing/generate_site.py
Normal file
282
voussoir.net/writing/generate_site.py
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
import os
|
||||||
|
import bs4
|
||||||
|
import etiquette
|
||||||
|
import pprint
|
||||||
|
import vmarkdown
|
||||||
|
import jinja2
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from voussoirkit import pathclass
|
||||||
|
from voussoirkit import spinal
|
||||||
|
from voussoirkit import winwhich
|
||||||
|
|
||||||
|
P = etiquette.photodb.PhotoDB(ephemeral=True)
|
||||||
|
P.log.setLevel(100)
|
||||||
|
|
||||||
|
writing_rootdir = pathclass.Path(__file__).parent
|
||||||
|
|
||||||
|
def write(path, content):
|
||||||
|
path = pathclass.Path(path)
|
||||||
|
if path not in writing_rootdir:
|
||||||
|
raise ValueError(path)
|
||||||
|
print(path.absolute_path)
|
||||||
|
f = open(path.absolute_path, 'w', encoding='utf-8')
|
||||||
|
f.write(content)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
GIT = winwhich.which('git')
|
||||||
|
|
||||||
|
def git_repo_for_file(path):
|
||||||
|
path = pathclass.Path(path)
|
||||||
|
folder = path.parent
|
||||||
|
prev = None
|
||||||
|
while folder != prev:
|
||||||
|
if folder.with_child('.git').exists:
|
||||||
|
return folder
|
||||||
|
prev = folder
|
||||||
|
folder = folder.parent
|
||||||
|
raise Exception('No Git repo.')
|
||||||
|
|
||||||
|
def git_file_date(path):
|
||||||
|
path = pathclass.Path(path)
|
||||||
|
repo = git_repo_for_file(path)
|
||||||
|
path = path.relative_to(repo, simple=True)
|
||||||
|
command = [
|
||||||
|
GIT,
|
||||||
|
'-C', repo.absolute_path,
|
||||||
|
'log',
|
||||||
|
'--diff-filter=A',
|
||||||
|
'--pretty=format:%ad',
|
||||||
|
'--date=short',
|
||||||
|
'--', path,
|
||||||
|
]
|
||||||
|
# print(command)
|
||||||
|
output = subprocess.check_output(command, stderr=subprocess.PIPE).decode('utf-8')
|
||||||
|
return output
|
||||||
|
|
||||||
|
class Article:
|
||||||
|
def __init__(self, md_file):
|
||||||
|
self.md_file = pathclass.Path(md_file)
|
||||||
|
self.html_file = self.md_file.replace_extension('html')
|
||||||
|
self.web_path = self.md_file.parent.relative_to(writing_rootdir, simple=True)
|
||||||
|
self.date = git_file_date(self.md_file)
|
||||||
|
|
||||||
|
self.soup = vmarkdown.markdown(
|
||||||
|
self.md_file.absolute_path,
|
||||||
|
css=writing_rootdir.with_child('css').with_child('dark.css').absolute_path,
|
||||||
|
return_soup=True,
|
||||||
|
templates=writing_rootdir.with_child('headerfooter.md').absolute_path,
|
||||||
|
)
|
||||||
|
self.title = self.soup.head.title.get_text()
|
||||||
|
|
||||||
|
tag_links = self.soup.find_all('a', {'class': 'tag_link'})
|
||||||
|
for tag_link in tag_links:
|
||||||
|
tagname = tag_link['data-qualname'].split('.')[-1]
|
||||||
|
tag_link['href'] = f'/writing/tags/{tagname}'
|
||||||
|
|
||||||
|
self.tags = [a['data-qualname'] for a in tag_links]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Article({self.md_file.absolute_path})'
|
||||||
|
|
||||||
|
ARTICLES = {file: Article(file) for file in spinal.walk_generator(writing_rootdir) if file.extension == 'md' and file.parent != writing_rootdir}
|
||||||
|
|
||||||
|
def write_articles():
|
||||||
|
for article in ARTICLES.values():
|
||||||
|
if article.md_file.replace_extension('').basename != article.md_file.parent.basename:
|
||||||
|
print(f'Warning: {article} does not match folder name.')
|
||||||
|
|
||||||
|
for qualname in article.tags:
|
||||||
|
P.easybake(qualname)
|
||||||
|
|
||||||
|
P.new_photo(article.md_file.absolute_path, tags=article.tags)
|
||||||
|
html = str(article.soup)
|
||||||
|
write(article.html_file.absolute_path, html)
|
||||||
|
|
||||||
|
class Index:
|
||||||
|
def __init__(self):
|
||||||
|
self.articles = []
|
||||||
|
self.children = {}
|
||||||
|
|
||||||
|
def navigate(self, query, create=False):
|
||||||
|
dest = self
|
||||||
|
while query:
|
||||||
|
parent = query[0]
|
||||||
|
if create:
|
||||||
|
dest = dest.children.setdefault(parent, Index())
|
||||||
|
else:
|
||||||
|
dest = dest.children.get(parent)
|
||||||
|
if not dest:
|
||||||
|
return
|
||||||
|
query = query[1:]
|
||||||
|
return dest
|
||||||
|
|
||||||
|
def assign(self, query, articles):
|
||||||
|
self.navigate(query, create=True).articles = articles
|
||||||
|
|
||||||
|
def get(self, query):
|
||||||
|
dest = self.navigate(query, create=False)
|
||||||
|
if dest:
|
||||||
|
return dest.articles
|
||||||
|
return []
|
||||||
|
|
||||||
|
def remove_redundant(query):
|
||||||
|
seen = set()
|
||||||
|
newq = tuple()
|
||||||
|
for tag in query:
|
||||||
|
if tag in seen:
|
||||||
|
continue
|
||||||
|
newq += (tag,)
|
||||||
|
seen.add(tag)
|
||||||
|
seen.update(tag.walk_parents())
|
||||||
|
return newq
|
||||||
|
|
||||||
|
def permute(query, pool):
|
||||||
|
if query:
|
||||||
|
query = remove_redundant(query)
|
||||||
|
if complete_tag_index.get(query):
|
||||||
|
return
|
||||||
|
articles = list(P.search(tag_musts=query))
|
||||||
|
if not articles:
|
||||||
|
return
|
||||||
|
articles = [ARTICLES[article.real_path] for article in articles]
|
||||||
|
|
||||||
|
if len(query) > 1:
|
||||||
|
previous = query[:-1]
|
||||||
|
prevarticles = complete_tag_index.get(previous)
|
||||||
|
# print(f'''
|
||||||
|
# query={query},
|
||||||
|
# docs={docs}
|
||||||
|
# previous={previous},
|
||||||
|
# prevdocs={prevdocs},
|
||||||
|
# ''')
|
||||||
|
if set(articles) == set(prevarticles):
|
||||||
|
return
|
||||||
|
s = str(query)
|
||||||
|
if 'python' in s and 'java' in s:
|
||||||
|
print('BAD', query, articles)
|
||||||
|
complete_tag_index.assign(query, articles)
|
||||||
|
# pprint.pprint(complete_tag_index)
|
||||||
|
# complete_tag_index[query] = docs
|
||||||
|
# print(query, pool, docs)
|
||||||
|
|
||||||
|
for tag in pool:
|
||||||
|
rest = pool.copy()
|
||||||
|
rest.remove(tag)
|
||||||
|
q = query + (tag,)
|
||||||
|
permute(q, rest)
|
||||||
|
|
||||||
|
|
||||||
|
def maketagpage(index, path):
|
||||||
|
path = [tag.name for tag in path]
|
||||||
|
parent = path[:-1]
|
||||||
|
parent = '/'.join(parent)
|
||||||
|
path = '/'.join(path)
|
||||||
|
|
||||||
|
page = jinja2.Template('''
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="stylesheet" href="/writing/css/dark.css"/>
|
||||||
|
<style>
|
||||||
|
body
|
||||||
|
{
|
||||||
|
display:grid;
|
||||||
|
grid-template:
|
||||||
|
"tagnav tagnav"
|
||||||
|
"articles refine";
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section style="grid-area:tagnav">
|
||||||
|
<p><a href="/writing">Back to writing</a></p>
|
||||||
|
{% if parent %}
|
||||||
|
<a href="/writing/tags/{{parent}}">Back to {{parent}}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/writing/tags">Back to tags</a>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% if index.articles %}
|
||||||
|
<section style="grid-area:articles">
|
||||||
|
<h1>{{path}}</h1>
|
||||||
|
<ul>
|
||||||
|
{% for article in index.articles %}
|
||||||
|
<li>
|
||||||
|
<a href="/writing/{{article.web_path}}">{{article.md_file.parent.basename}}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if index.children %}
|
||||||
|
<section style="grid-area:refine">
|
||||||
|
<h1>Refine your query</h1>
|
||||||
|
<ul>
|
||||||
|
{% for refine in children %}
|
||||||
|
<li>
|
||||||
|
{% if path %}
|
||||||
|
<a href="/writing/tags/{{path}}/{{refine}}">{{refine}}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/writing/tags/{{refine}}">{{refine}}</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
''').render(
|
||||||
|
parent=parent,
|
||||||
|
index=index,
|
||||||
|
path=path,
|
||||||
|
children=sorted(tag.name for tag in index.children.keys()),
|
||||||
|
)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def outs(index, path=[]):
|
||||||
|
filepath = ['tags'] + [tag.name for tag in path] + ['index.html']
|
||||||
|
for (child_name, child_index) in index.children.items():
|
||||||
|
outs(child_index, path=path+[child_name])
|
||||||
|
page = maketagpage(index, path)
|
||||||
|
filepath = os.sep.join(filepath)
|
||||||
|
filepath = writing_rootdir.join(filepath)
|
||||||
|
os.makedirs(filepath.parent.absolute_path, exist_ok=True)
|
||||||
|
write(filepath, page)
|
||||||
|
|
||||||
|
def write_tag_pages():
|
||||||
|
outs(complete_tag_index)
|
||||||
|
|
||||||
|
|
||||||
|
def write_writing_index():
|
||||||
|
page = jinja2.Template('''
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="stylesheet" href="/writing/css/dark.css"/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Writing</h1>
|
||||||
|
<ul>
|
||||||
|
{% for article in articles %}
|
||||||
|
<li>
|
||||||
|
<a href="{{article.web_path}}">{{article.date}} - {{article.title}}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
''').render(
|
||||||
|
articles=sorted(ARTICLES.values(), key=lambda a: a.date, reverse=True),
|
||||||
|
)
|
||||||
|
write(writing_rootdir.with_child('index.html'), page)
|
||||||
|
|
||||||
|
write_articles()
|
||||||
|
complete_tag_index = Index()
|
||||||
|
all_tags = set(P.get_tags())
|
||||||
|
permute(tuple(), all_tags)
|
||||||
|
write_tag_pages()
|
||||||
|
write_writing_index()
|
3
voussoir.net/writing/headerfooter.md
Normal file
3
voussoir.net/writing/headerfooter.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[Back to writing](/writing)
|
||||||
|
|
||||||
|
{body}
|
667
voussoir.net/writing/vmarkdown.py
Normal file
667
voussoir.net/writing/vmarkdown.py
Normal file
|
@ -0,0 +1,667 @@
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import bs4
|
||||||
|
import copy
|
||||||
|
import html
|
||||||
|
import mimetypes
|
||||||
|
import mistune
|
||||||
|
import os
|
||||||
|
import pygments
|
||||||
|
import pygments.formatters
|
||||||
|
import pygments.lexers
|
||||||
|
import pygments.token
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from voussoirkit import pathclass
|
||||||
|
|
||||||
|
HTML_TEMPLATE = '''
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
{css}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{body}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''.strip()
|
||||||
|
|
||||||
|
SLUG_CHARACTERS = string.ascii_lowercase + string.digits + '_'
|
||||||
|
|
||||||
|
|
||||||
|
class SyntaxHighlighting:
|
||||||
|
def block_code(self, text, lang):
|
||||||
|
inlinestyles = self.options.get('inlinestyles') or False
|
||||||
|
linenos = self.options.get('linenos') or False
|
||||||
|
return self._block_code(text, lang, inlinestyles, linenos)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _block_code(text, lang, inlinestyles=False, linenos=False):
|
||||||
|
if not lang:
|
||||||
|
text = text.strip()
|
||||||
|
return f'<pre><code>{mistune.escape(text)}</code></pre>\n'
|
||||||
|
try:
|
||||||
|
lexer = pygments.lexers.get_lexer_by_name(lang.lower(), stripall=True)
|
||||||
|
# if isinstance(lexer, pygments.lexers.PythonLexer):
|
||||||
|
# lexer = pygments.lexers.PythonConsoleLexer()
|
||||||
|
|
||||||
|
# But wait! Why aren't you doing this:
|
||||||
|
# formatter = pygments.formatters.HtmlFormatter(
|
||||||
|
# noclasses=inlinestyles,
|
||||||
|
# linenos=linenos,
|
||||||
|
# cssclass='highlight ' + (lang.lower() if lang else ''),
|
||||||
|
# )
|
||||||
|
# code = pygments.highlight(text, lexer, formatter).decode('utf-8')
|
||||||
|
# ??
|
||||||
|
elements = []
|
||||||
|
for (token, text) in lexer.get_tokens(text):
|
||||||
|
if text.isspace():
|
||||||
|
elements.append(text)
|
||||||
|
continue
|
||||||
|
css_class = pygments.token.STANDARD_TYPES.get(token, '')
|
||||||
|
element = f'<span class="{css_class}">{html.escape(text)}</span>'
|
||||||
|
elements.append(element)
|
||||||
|
code = ''.join(elements)
|
||||||
|
|
||||||
|
divclass = ['highlight']
|
||||||
|
if lang:
|
||||||
|
divclass.append(lang.lower())
|
||||||
|
divclass = ' '.join(divclass)
|
||||||
|
|
||||||
|
code = f'<div class="{divclass}"><pre>{code}</pre></div>'
|
||||||
|
# if lang:
|
||||||
|
# code = code.replace('div class="highlight"', f'div class="highlight {lang.lower()}"')
|
||||||
|
# if linenos:
|
||||||
|
# return f'<div class="highlight-wrapper">{code}</div>\n'
|
||||||
|
return code
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
return f'<pre class="{lang}"><code>{mistune.escape(text)}</code></pre>\n'
|
||||||
|
|
||||||
|
|
||||||
|
class VoussoirRenderer(
|
||||||
|
SyntaxHighlighting,
|
||||||
|
mistune.Renderer,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class VoussoirGrammar(mistune.InlineGrammar):
|
||||||
|
larr = re.compile(r'<--')
|
||||||
|
rarr = re.compile(r'-->')
|
||||||
|
mdash = re.compile(r'--')
|
||||||
|
category_tag = re.compile(r'\[tag:([\w\.]+)\]')
|
||||||
|
text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~\-]|https?:\/\/| {2,}\n|$)')
|
||||||
|
|
||||||
|
class VoussoirLexer(mistune.InlineLexer):
|
||||||
|
default_rules = copy.copy(mistune.InlineLexer.default_rules)
|
||||||
|
default_rules.insert(0, 'mdash')
|
||||||
|
default_rules.insert(0, 'larr')
|
||||||
|
default_rules.insert(0, 'rarr')
|
||||||
|
default_rules.insert(0, 'category_tag')
|
||||||
|
|
||||||
|
def __init__(self, renderer, **kwargs):
|
||||||
|
rules = VoussoirGrammar()
|
||||||
|
super().__init__(renderer, rules, **kwargs)
|
||||||
|
|
||||||
|
def output_category_tag(self, m):
|
||||||
|
qualname = m.group(1)
|
||||||
|
tagname = qualname.split('.')[-1]
|
||||||
|
return f'<a class="tag_link" data-qualname="{qualname}">[{tagname}]</a>'
|
||||||
|
|
||||||
|
def output_mdash(self, m):
|
||||||
|
return '—'
|
||||||
|
|
||||||
|
def output_rarr(self, m):
|
||||||
|
return '→'
|
||||||
|
|
||||||
|
def output_larr(self, m):
|
||||||
|
return '←'
|
||||||
|
|
||||||
|
renderer = VoussoirRenderer()
|
||||||
|
inline = VoussoirLexer(renderer)
|
||||||
|
VMARKDOWN = mistune.Markdown(renderer=renderer, inline=inline)
|
||||||
|
|
||||||
|
# GENERIC HELPERS
|
||||||
|
################################################################################
|
||||||
|
def cat_file(path):
|
||||||
|
if isinstance(path, pathclass.Path):
|
||||||
|
path = path.absolute_path
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def cat_files(paths):
|
||||||
|
if not paths:
|
||||||
|
return ''
|
||||||
|
if isinstance(paths, str):
|
||||||
|
return cat_file(paths)
|
||||||
|
content = [cat_file(path) for path in paths]
|
||||||
|
return '\n\n'.join(content)
|
||||||
|
|
||||||
|
def dump_file(path):
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
# SOUP HELPERS
|
||||||
|
################################################################################
|
||||||
|
def add_header_anchors(soup):
|
||||||
|
'''
|
||||||
|
Give each <hX> an <a> to link to it.
|
||||||
|
'''
|
||||||
|
header_pattern = re.compile(rf'^h[1-6]$')
|
||||||
|
used_slugs = set()
|
||||||
|
|
||||||
|
for header in soup.find_all(header_pattern):
|
||||||
|
slug = slugify(header.get_text())
|
||||||
|
slug = uniqify_slug(slug, used_slugs)
|
||||||
|
|
||||||
|
header['id'] = slug
|
||||||
|
|
||||||
|
new_a = soup.new_tag('a')
|
||||||
|
new_a['href'] = '#' + slug
|
||||||
|
new_a['class'] = 'header_anchor_link'
|
||||||
|
paragraph_symbol = chr(182)
|
||||||
|
new_a.append(f' ({paragraph_symbol})')
|
||||||
|
header.append(new_a)
|
||||||
|
|
||||||
|
def add_toc(soup, max_level=None):
|
||||||
|
'''
|
||||||
|
Gather up all the header anchors and form a table of contents,
|
||||||
|
which will be placed below the first h1 on the page, if the page has an h1.
|
||||||
|
'''
|
||||||
|
first_h1 = soup.h1
|
||||||
|
if not first_h1:
|
||||||
|
return
|
||||||
|
|
||||||
|
def new_list(root=False):
|
||||||
|
r = bs4.BeautifulSoup('<ol></ol>', 'html.parser')
|
||||||
|
if root:
|
||||||
|
return r
|
||||||
|
return r.ol
|
||||||
|
|
||||||
|
# Official HTML headers only go up to 6.
|
||||||
|
if max_level is None:
|
||||||
|
max_level = 6
|
||||||
|
|
||||||
|
elif max_level < 1:
|
||||||
|
raise ValueError('max_level must be >= 1.')
|
||||||
|
|
||||||
|
header_pattern = re.compile(rf'^h[1-{max_level}]$')
|
||||||
|
|
||||||
|
toc = new_list(root=True)
|
||||||
|
toc.ol['id'] = 'table_of_contents'
|
||||||
|
toc.ol.append('Table of contents')
|
||||||
|
current_list = toc.ol
|
||||||
|
current_list['level'] = None
|
||||||
|
|
||||||
|
headers = soup.find_all(header_pattern)
|
||||||
|
for header in headers:
|
||||||
|
if header == first_h1:
|
||||||
|
continue
|
||||||
|
# 'hX' -> X
|
||||||
|
level = int(header.name[1])
|
||||||
|
|
||||||
|
toc_line = toc.new_tag('li')
|
||||||
|
toc_a = toc.new_tag('a')
|
||||||
|
|
||||||
|
toc_a.append(str(header.find(text=True)))
|
||||||
|
toc_a['href'] = f'#{header["id"]}'
|
||||||
|
toc_line.append(toc_a)
|
||||||
|
|
||||||
|
if current_list['level'] is None:
|
||||||
|
current_list['level'] = level
|
||||||
|
|
||||||
|
while level < current_list['level']:
|
||||||
|
# Because the sub-<ol> are actually a child of the last
|
||||||
|
# <li> of the previous <ol>, we must .parent twice.
|
||||||
|
# The second .parent is conditional because if the current
|
||||||
|
# list is toc.ol, then parent is a Soup document object, and
|
||||||
|
# parenting again would be a mistake. We'll recover from
|
||||||
|
# this in just a moment.
|
||||||
|
current_list = current_list.parent
|
||||||
|
if current_list.name == 'li':
|
||||||
|
current_list = current_list.parent
|
||||||
|
# If the file has headers in a non-ascending order, like the
|
||||||
|
# first header is an h4 and then an h1 comes later, then
|
||||||
|
# this while loop would keep attempting to climb the .parent
|
||||||
|
# which would take us too far, off the top of the tree.
|
||||||
|
# So, if we reach `current_list == toc.ol` then we've
|
||||||
|
# reached the root and should stop climbing. At that point
|
||||||
|
# we can just snap current_level and use the root list again.
|
||||||
|
# In the resulting toc, that initial h4 would have the same
|
||||||
|
# toc depth as the later h1 since it never had parents.
|
||||||
|
if current_list == toc:
|
||||||
|
current_list['level'] = level
|
||||||
|
current_list = toc.ol
|
||||||
|
|
||||||
|
if level > current_list['level']:
|
||||||
|
# In order to properly render nested <ol>, you're supposed
|
||||||
|
# to make the new <ol> a child of the last <li> of the
|
||||||
|
# previous <ol>. NOT a child of the prev <ol> directly.
|
||||||
|
# Don't worry, .children can never be empty because on the
|
||||||
|
# first <li> this condition can never occur, and new <ol>s
|
||||||
|
# always receive a child right after being created.
|
||||||
|
_l = new_list()
|
||||||
|
_l['level'] = level
|
||||||
|
final_li = list(current_list.children)[-1]
|
||||||
|
final_li.append(_l)
|
||||||
|
current_list = _l
|
||||||
|
|
||||||
|
current_list.append(toc_line)
|
||||||
|
|
||||||
|
for ol in toc.find_all('ol'):
|
||||||
|
del ol['level']
|
||||||
|
|
||||||
|
first_h1.insert_after(toc.ol)
|
||||||
|
|
||||||
|
def add_head_title(soup):
|
||||||
|
'''
|
||||||
|
Add the <title> element in <head> based on the text of the first <h1>.
|
||||||
|
'''
|
||||||
|
first_h1 = soup.h1
|
||||||
|
if not first_h1:
|
||||||
|
return
|
||||||
|
|
||||||
|
text = get_innertext(first_h1)
|
||||||
|
title = soup.new_tag('title')
|
||||||
|
title.append(text)
|
||||||
|
soup.head.append(title)
|
||||||
|
|
||||||
|
def embed_images(soup, cache=None):
|
||||||
|
'''
|
||||||
|
Find <img> srcs and either download the url or load the local file,
|
||||||
|
and convert it to a data URI.
|
||||||
|
'''
|
||||||
|
for element in soup.find_all('img'):
|
||||||
|
src = element['src']
|
||||||
|
if cache is None:
|
||||||
|
cache = {}
|
||||||
|
if cache.get(src) is None:
|
||||||
|
print('Fetching %s' % src)
|
||||||
|
if src.startswith('https://') or src.startswith('http://'):
|
||||||
|
response = requests.get(src)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.content
|
||||||
|
else:
|
||||||
|
data = dump_file(src)
|
||||||
|
data = base64.b64encode(data).decode('ascii')
|
||||||
|
mime = mimetypes.guess_type(src)[0]
|
||||||
|
mime = mime if mime is not None else ''
|
||||||
|
uri = f'data:{mime};base64,{data}'
|
||||||
|
cache[src] = uri
|
||||||
|
else:
|
||||||
|
uri = cache[src]
|
||||||
|
element['src'] = uri
|
||||||
|
|
||||||
|
def get_innertext(element):
|
||||||
|
if isinstance(element, bs4.NavigableString):
|
||||||
|
return element.string
|
||||||
|
else:
|
||||||
|
return element.get_text()
|
||||||
|
|
||||||
|
def next_element_sibling(element):
|
||||||
|
'''
|
||||||
|
Like nextSibling but skips NavigableString.
|
||||||
|
'''
|
||||||
|
while True:
|
||||||
|
element = element.nextSibling
|
||||||
|
if isinstance(element, bs4.NavigableString):
|
||||||
|
continue
|
||||||
|
return element
|
||||||
|
|
||||||
|
def previous_element_sibling(element):
|
||||||
|
while True:
|
||||||
|
element = element.previousSibling
|
||||||
|
if isinstance(element, bs4.NavigableString):
|
||||||
|
continue
|
||||||
|
return element
|
||||||
|
|
||||||
|
def remove_leading_empty_nodes(element):
|
||||||
|
'''
|
||||||
|
Code <pre>s often start with an empty span, so this strips it off.
|
||||||
|
'''
|
||||||
|
children = list(element.children)
|
||||||
|
while children:
|
||||||
|
if get_innertext(children[0]) == '':
|
||||||
|
children.pop(0).extract()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
def slugify(text):
|
||||||
|
'''
|
||||||
|
Filter text to contain only SLUG_CHARACTERS.
|
||||||
|
'''
|
||||||
|
text = text.lower()
|
||||||
|
text = text.replace(' ', '_')
|
||||||
|
text = [c for c in text if c in SLUG_CHARACTERS]
|
||||||
|
text = ''.join(text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
def uniqify_slug(slug, used_slugs):
|
||||||
|
'''
|
||||||
|
If the given slug has already been used, give it a trailing _2 or _3 etc.
|
||||||
|
'''
|
||||||
|
count = 2
|
||||||
|
try_slug = slug
|
||||||
|
while try_slug in used_slugs:
|
||||||
|
try_slug = f'{slug}_{count}'
|
||||||
|
count += 1
|
||||||
|
slug = try_slug
|
||||||
|
used_slugs.add(slug)
|
||||||
|
return slug
|
||||||
|
|
||||||
|
# HTML CLEANERS
|
||||||
|
################################################################################
|
||||||
|
def html_replacements(html):
|
||||||
|
html = re.sub(r'<style>\s*</style>', '', html)
|
||||||
|
html = html.replace(
|
||||||
|
'<span class="o">>></span><span class="o">></span>',
|
||||||
|
'<span>>>></span>'
|
||||||
|
)
|
||||||
|
html = html.replace(
|
||||||
|
'<span class="o">.</span><span class="o">.</span><span class="o">.</span>',
|
||||||
|
'<span>...</span>'
|
||||||
|
)
|
||||||
|
return html
|
||||||
|
|
||||||
|
# SOUP CLEANERS
|
||||||
|
################################################################################
|
||||||
|
def fix_argument_call_classes(element):
|
||||||
|
'''
|
||||||
|
Given a <span class="n"> pointing to a function being called, this fixes
|
||||||
|
the classes of all the keyword arguments from being plain names to being
|
||||||
|
argument names.
|
||||||
|
'''
|
||||||
|
# print('INPUT', repr(element))
|
||||||
|
paren_depth = 0
|
||||||
|
while True:
|
||||||
|
element = next_element_sibling(element)
|
||||||
|
# print(element, paren_depth)
|
||||||
|
innertext = element.get_text()
|
||||||
|
|
||||||
|
if innertext == '(':
|
||||||
|
paren_depth += 1
|
||||||
|
|
||||||
|
if innertext == ')':
|
||||||
|
paren_depth -= 1
|
||||||
|
|
||||||
|
if 'n' in element['class']:
|
||||||
|
last_known_candidate = element
|
||||||
|
|
||||||
|
if 'o' in element['class'] and innertext == '=':
|
||||||
|
last_known_candidate['class'].remove('n')
|
||||||
|
last_known_candidate['class'].append('narg')
|
||||||
|
|
||||||
|
if paren_depth == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
def fix_argument_def_classes(element):
|
||||||
|
'''
|
||||||
|
Given a <span class="kd">def</span>, fix the function arguments so they are
|
||||||
|
a special color like they're SUPPOSED TO BE.
|
||||||
|
'''
|
||||||
|
# print('INPUT', repr(element))
|
||||||
|
do_color = True
|
||||||
|
while True:
|
||||||
|
element = next_element_sibling(element)
|
||||||
|
# print(element)
|
||||||
|
innertext = element.get_text()
|
||||||
|
if innertext == ')' and next_element_sibling(element).get_text() == ':':
|
||||||
|
break
|
||||||
|
|
||||||
|
if innertext == '=':
|
||||||
|
do_color = False
|
||||||
|
|
||||||
|
elif innertext == ',':
|
||||||
|
do_color = True
|
||||||
|
|
||||||
|
elif do_color:
|
||||||
|
if 'n' in element['class']:
|
||||||
|
element['class'].remove('n')
|
||||||
|
element['class'].append('narg')
|
||||||
|
elif 'bp' in element['class']:
|
||||||
|
element['class'].remove('bp')
|
||||||
|
element['class'].append('narg')
|
||||||
|
elif 'o' in element['class'] and innertext in ('*', '**'):
|
||||||
|
# Fix *args, the star should not be operator colored.
|
||||||
|
element['class'].remove('o')
|
||||||
|
element['class'].append('n')
|
||||||
|
|
||||||
|
def fix_repl_classes(element):
|
||||||
|
'''
|
||||||
|
Given a <pre> element, this function detects that this pre contains a REPL
|
||||||
|
session when the first line starts with '>>>'.
|
||||||
|
|
||||||
|
For REPL sessions, any elements on an input line (which start with '>>>' or
|
||||||
|
'...') keep their styles, while elements on output lines are stripped of
|
||||||
|
their styles.
|
||||||
|
|
||||||
|
Of course you can confuse it by having an output which starts with '>>>'
|
||||||
|
but that's not the point okay?
|
||||||
|
'''
|
||||||
|
remove_leading_empty_nodes(element)
|
||||||
|
children = list(element.children)
|
||||||
|
if not children:
|
||||||
|
return
|
||||||
|
|
||||||
|
if get_innertext(children[0]) != '>>>':
|
||||||
|
return
|
||||||
|
|
||||||
|
del_styles = None
|
||||||
|
for child in children:
|
||||||
|
if get_innertext(child).endswith('\n'):
|
||||||
|
del_styles = None
|
||||||
|
|
||||||
|
elif del_styles is None:
|
||||||
|
del_styles = child.string not in ('>>>', '...')
|
||||||
|
|
||||||
|
if isinstance(child, bs4.NavigableString):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if del_styles:
|
||||||
|
del child['class']
|
||||||
|
|
||||||
|
def fix_leading_pre_spaces(element):
|
||||||
|
'''
|
||||||
|
I noticed this issue when using code blocks inside of a numbered list.
|
||||||
|
The first line would be okay but then the rest of the lines would be
|
||||||
|
+1 or +2 spaces indented.
|
||||||
|
So this looks for linebreaks inside code blocks, and removes additional
|
||||||
|
spaces that come after the linebreak.
|
||||||
|
'''
|
||||||
|
return
|
||||||
|
children = list(element.children)
|
||||||
|
for child in children:
|
||||||
|
if isinstance(child, bs4.element.NavigableString):
|
||||||
|
text = get_innertext(child)
|
||||||
|
text = text.split('\n')
|
||||||
|
text = [text[0]] + [t.lstrip() for t in text[1:]]
|
||||||
|
text = '\n'.join(text)
|
||||||
|
child.replace_with(text)
|
||||||
|
|
||||||
|
def fix_classes(soup):
|
||||||
|
'''
|
||||||
|
Because pygments does not conform to my standards of beauty already!
|
||||||
|
'''
|
||||||
|
for element in soup.find_all('span', {'class': 'k'}):
|
||||||
|
if get_innertext(element) in ('def', 'class'):
|
||||||
|
element['class'] = ['kd']
|
||||||
|
|
||||||
|
for element in soup.find_all('span', {'class': 'bp'}):
|
||||||
|
if get_innertext(element) in ('None', 'True', 'False'):
|
||||||
|
element['class'] = ['m']
|
||||||
|
|
||||||
|
for element in soup.find_all('span', {'class': 'o'}):
|
||||||
|
if get_innertext(element) in ('.', '(', ')', '[', ']', '{', '}', ';', ','):
|
||||||
|
element['class'] = ['n']
|
||||||
|
|
||||||
|
for element in soup.find_all('pre'):
|
||||||
|
fix_repl_classes(element)
|
||||||
|
fix_leading_pre_spaces(element)
|
||||||
|
|
||||||
|
for element in soup.find_all('span', {'class': 'kd'}):
|
||||||
|
if element.get_text() == 'def':
|
||||||
|
fix_argument_def_classes(element)
|
||||||
|
|
||||||
|
for element in soup.find_all('span', {'class': 'n'}):
|
||||||
|
if get_innertext(element.nextSibling) == '(':
|
||||||
|
fix_argument_call_classes(element)
|
||||||
|
|
||||||
|
# FINAL MARKDOWNS
|
||||||
|
################################################################################
|
||||||
|
def markdown(
|
||||||
|
filename,
|
||||||
|
*,
|
||||||
|
css=None,
|
||||||
|
do_embed_images=False,
|
||||||
|
image_cache=None,
|
||||||
|
return_soup=False,
|
||||||
|
templates=None,
|
||||||
|
):
|
||||||
|
body = cat_file(filename)
|
||||||
|
|
||||||
|
if templates:
|
||||||
|
if isinstance(templates, str):
|
||||||
|
templates = [templates]
|
||||||
|
for template in templates:
|
||||||
|
template = cat_file(template)
|
||||||
|
body = template.replace('{body}', body)
|
||||||
|
|
||||||
|
css = cat_files(css)
|
||||||
|
|
||||||
|
body = VMARKDOWN(body)
|
||||||
|
html = HTML_TEMPLATE.format(css=css, body=body)
|
||||||
|
|
||||||
|
html = html_replacements(html)
|
||||||
|
|
||||||
|
soup = bs4.BeautifulSoup(html, 'html.parser')
|
||||||
|
# Make sure to add_head_title before add_header_anchors so you don't get
|
||||||
|
# the paragraph symbol in the <title>.
|
||||||
|
add_head_title(soup)
|
||||||
|
add_header_anchors(soup)
|
||||||
|
add_toc(soup)
|
||||||
|
fix_classes(soup)
|
||||||
|
if do_embed_images:
|
||||||
|
embed_images(soup, cache=image_cache)
|
||||||
|
|
||||||
|
|
||||||
|
if return_soup:
|
||||||
|
return soup
|
||||||
|
|
||||||
|
html = str(soup)
|
||||||
|
return html
|
||||||
|
|
||||||
|
def markdown_flask(core_filename, port, *args, **kwargs):
|
||||||
|
import flask
|
||||||
|
from flask import request
|
||||||
|
site = flask.Flask(__name__)
|
||||||
|
image_cache = {}
|
||||||
|
kwargs['image_cache'] = image_cache
|
||||||
|
core_filename = pathclass.Path(core_filename, force_sep='/')
|
||||||
|
if core_filename.is_dir:
|
||||||
|
cwd = core_filename
|
||||||
|
else:
|
||||||
|
cwd = pathclass.Path('.')
|
||||||
|
|
||||||
|
def handle_path(path):
|
||||||
|
if path.extension == '.md':
|
||||||
|
return do_md_for(path)
|
||||||
|
|
||||||
|
if path.is_dir:
|
||||||
|
atags = []
|
||||||
|
for child in path.listdir():
|
||||||
|
relative = child.relative_to(cwd, simple=True)
|
||||||
|
print(relative)
|
||||||
|
a = f'<p><a href="/{relative}">{child.basename}</a></p>'
|
||||||
|
atags.append(a)
|
||||||
|
page = '\n'.join(atags)
|
||||||
|
return page
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = open(path.absolute_path, 'rb').read()
|
||||||
|
except Exception as exc:
|
||||||
|
print(exc)
|
||||||
|
flask.abort(404)
|
||||||
|
else:
|
||||||
|
response = flask.make_response(content)
|
||||||
|
|
||||||
|
mime = mimetypes.guess_type(path.absolute_path)[0]
|
||||||
|
if mime:
|
||||||
|
response.headers['Content-Type'] = mime
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def do_md_for(filename):
|
||||||
|
html = markdown(filename=filename, *args, **kwargs)
|
||||||
|
refresh = request.args.get('refresh', None)
|
||||||
|
if refresh is not None:
|
||||||
|
refresh = max(float(refresh), 1)
|
||||||
|
html += f'<script>setTimeout(function(){{window.location.reload()}}, {refresh * 1000})</script>'
|
||||||
|
return html
|
||||||
|
|
||||||
|
@site.route('/')
|
||||||
|
def root():
|
||||||
|
return handle_path(core_filename)
|
||||||
|
|
||||||
|
@site.route('/<path:path>')
|
||||||
|
def other_file(path):
|
||||||
|
path = cwd.join(path)
|
||||||
|
if path not in cwd:
|
||||||
|
flask.abort(404)
|
||||||
|
return handle_path(path)
|
||||||
|
|
||||||
|
site.run(host='0.0.0.0', port=port)
|
||||||
|
|
||||||
|
# COMMAND LINE
|
||||||
|
################################################################################
|
||||||
|
def markdown_argparse(args):
|
||||||
|
if args.output_filename:
|
||||||
|
md_file = pathclass.Path(args.md_filename)
|
||||||
|
output_file = pathclass.Path(args.output_filename)
|
||||||
|
if md_file == output_file:
|
||||||
|
raise ValueError('md file and output file are the same!')
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'filename': args.md_filename,
|
||||||
|
'css': args.css,
|
||||||
|
'do_embed_images': args.do_embed_images,
|
||||||
|
'templates': args.template,
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.server:
|
||||||
|
return markdown_flask(core_filename=kwargs.pop('filename'), port=args.server, **kwargs)
|
||||||
|
|
||||||
|
html = markdown(**kwargs)
|
||||||
|
|
||||||
|
if args.output_filename:
|
||||||
|
f = open(args.output_filename, 'w', encoding='utf-8')
|
||||||
|
f.write(html)
|
||||||
|
f.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
print(html)
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument('md_filename')
|
||||||
|
parser.add_argument('--css', dest='css', action='append', default=None)
|
||||||
|
parser.add_argument('--template', dest='template', action='append', default=None)
|
||||||
|
parser.add_argument('--embed_images', dest='do_embed_images', action='store_true')
|
||||||
|
parser.add_argument('-o', '--output', dest='output_filename', default=None)
|
||||||
|
parser.add_argument('--server', dest='server', type=int, default=None)
|
||||||
|
parser.set_defaults(func=markdown_argparse)
|
||||||
|
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
Loading…
Reference in a new issue