"""
Shared helpers used by both the blog and git static site
generators.
"""
import os
import re
import html
TEMPLATE_DIR = "templates"
# The site header (name, nav, and Cross of Jerusalem mark) is
# identical on every page, so it lives here once instead of
# being duplicated across every template.
HEADER_HTML = """<header>
<a href="/" class="cross-link" aria-label="Home">
<svg class="cross" viewBox="0 0 100 100" aria-hidden="true">
<rect x="43" y="6" width="14" height="88"/>
<rect x="6" y="43" width="88" height="14"/>
<rect x="23" y="18" width="4" height="14"/>
<rect x="18" y="23" width="14" height="4"/>
<rect x="73" y="18" width="4" height="14"/>
<rect x="68" y="23" width="14" height="4"/>
<rect x="23" y="68" width="4" height="14"/>
<rect x="18" y="73" width="14" height="4"/>
<rect x="73" y="68" width="4" height="14"/>
<rect x="68" y="73" width="14" height="4"/>
</svg>
</a>
<div class="brand-text">
<h1><a href="/">Milán Major</a></h1>
<nav>
<a href="/">home</a>
<a href="/blog/">blog</a>
<a href="/git/">git</a>
<a href="/about.html">about</a>
</nav>
</div>
</header>"""
# ------------------------------------------------------------
# Markdown
#
# A small, dependency-free Markdown renderer shared by the blog
# (post bodies) and the git browser (README files). It covers
# the subset of Markdown commonly used in both: headings,
# paragraphs, code fences, lists, blockquotes, rules, links,
# images and inline emphasis/code. It doesn't handle nested
# lists or tables - kept deliberately simple.
# ------------------------------------------------------------
def inline_markdown(text):
text = html.escape(text)
text = re.sub(
r"!\[([^\]]*)\]\(([^)]+)\)",
r'<img src="\2" alt="\1">',
text
)
text = re.sub(
r"\[([^\]]+)\]\(([^)]+)\)",
r'<a href="\2">\1</a>',
text
)
text = re.sub(
r"\*\*(.+?)\*\*",
r"<strong>\1</strong>",
text
)
text = re.sub(
r"\*(.+?)\*",
r"<em>\1</em>",
text
)
text = re.sub(
r"`(.+?)`",
r"<code>\1</code>",
text
)
return text
def markdown(text, heading_offset=0):
"""
`heading_offset` shifts every heading level down (e.g. 1 turns
a "# Title" into <h2>), useful when embedding a document (such
as a README) below a page that already has its own <h1>.
"""
lines = text.splitlines()
output = []
paragraph = []
code_block = None
code_lang = None
list_kind = None
list_items = None
quote_lines = None
def flush_paragraph():
if not paragraph:
return
value = " ".join(paragraph)
output.append(f"<p>{inline_markdown(value)}</p>")
paragraph.clear()
def flush_list():
nonlocal list_kind, list_items
if list_kind is None:
return
items_html = "\n".join(
f"<li>{inline_markdown(item)}</li>" for item in list_items
)
output.append(f"<{list_kind}>\n{items_html}\n</{list_kind}>")
list_kind = None
list_items = None
def flush_quote():
nonlocal quote_lines
if quote_lines is None:
return
value = " ".join(quote_lines)
output.append(f"<blockquote><p>{inline_markdown(value)}</p></blockquote>")
quote_lines = None
for line in lines:
stripped = line.strip()
if code_block is not None:
if stripped.startswith("```"):
code = "\n".join(code_block)
lang_attr = (
f' class="language-{code_lang}"' if code_lang else ""
)
output.append(
f"<pre><code{lang_attr}>{html.escape(code)}</code></pre>"
)
code_block = None
code_lang = None
else:
code_block.append(line)
continue
if stripped.startswith("```"):
# A line that opens *and* closes a fence on its own
# (e.g. "``` some command ```") is a single-line code
# span, not the start of a multi-line block - treat
# it as such instead of swallowing every line up to
# the next stray "```" elsewhere in the document.
single_line = re.match(r"^`{3,}\s*(.+?)\s*`{3,}$", stripped)
if single_line and "```" not in single_line.group(1):
flush_paragraph()
flush_list()
flush_quote()
output.append(
f"<pre><code>{html.escape(single_line.group(1))}"
"</code></pre>"
)
continue
flush_paragraph()
flush_list()
flush_quote()
code_block = []
code_lang = stripped[3:].strip() or None
continue
if not stripped:
flush_paragraph()
flush_list()
flush_quote()
continue
if re.match(r"^(-{3,}|\*{3,}|_{3,})$", stripped):
flush_paragraph()
flush_list()
flush_quote()
output.append("<hr>")
continue
heading = re.match(r"^(#{1,6})\s+(.*)$", stripped)
if heading:
flush_paragraph()
flush_list()
flush_quote()
level = min(6, len(heading.group(1)) + heading_offset)
output.append(
f"<h{level}>{inline_markdown(heading.group(2))}</h{level}>"
)
continue
if stripped.startswith(">"):
flush_paragraph()
flush_list()
if quote_lines is None:
quote_lines = []
quote_lines.append(stripped[1:].strip())
continue
flush_quote()
ordered = re.match(r"^\d+\.\s+(.*)$", stripped)
unordered = re.match(r"^[-*+]\s+(.*)$", stripped)
if ordered or unordered:
flush_paragraph()
kind = "ol" if ordered else "ul"
item_text = (ordered or unordered).group(1)
if list_kind != kind:
flush_list()
list_kind = kind
list_items = []
list_items.append(item_text)
continue
flush_list()
paragraph.append(line)
flush_paragraph()
flush_list()
flush_quote()
if code_block is not None:
code = "\n".join(code_block)
output.append(f"<pre><code>{html.escape(code)}</code></pre>")
return "\n".join(output)
def load_template(name):
path = os.path.join(
TEMPLATE_DIR,
name
)
with open(path, "r", encoding="utf-8") as f:
return f.read()
def render(template, values):
values = {"header": HEADER_HTML, **values}
for key, value in values.items():
template = template.replace(
"{{ " + key + " }}",
str(value)
)
return template