#!/usr/bin/env python3
"""Render docs/ into a static site.
contrib/gen-docs [outdir] default: build/docs
SLOSH_DOCS=dir contrib/gen-docs out ...from another docs tree
No dependencies and no toolchain: this is one file of stdlib Python and one
stylesheet, for the same reason src/kdl.c is a hand-rolled KDL subset rather
than a library — the input is ours, we know exactly which constructs it uses,
and a docs build that needs a package manager is a docs build nobody runs.
What it understands is what docs/ actually contains: headings, paragraphs,
fenced code, pipe tables, lists (one level of nesting), blockquotes, rules,
`code`, **bold**, *italic*, and links. Anything else is passed through as text,
escaped. A construct nobody uses is not supported on purpose: it would be
untested code pretending to be a feature.
The nav — order, grouping — comes from docs/nav. Page titles come from each
page's own `# heading`, so they cannot drift from the page.
"""
import html
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
# $SLOSH_DOCS points it at another tree, which is how the tests drive it over
# fixtures -- including the trees that are supposed to be refused.
DOCS = os.environ.get("SLOSH_DOCS") or os.path.join(ROOT, "docs")
# ---- the markdown subset --------------------------------------------------
INLINE_CODE = re.compile(r"`([^`]+)`")
LINK = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
BOLD = re.compile(r"\*\*([^*]+)\*\*")
ITALIC = re.compile(r"(?%s' % (link_href(m.group(2)), m.group(1)), text
)
text = BOLD.sub(r"\1", text)
text = ITALIC.sub(r"\1", text)
for i, code in enumerate(spans):
text = text.replace("\0%d\0" % i, "%s" % code)
return text
def link_href(href):
"""A link to another page becomes a link to its rendered page."""
if href.startswith(("http://", "https://", "#", "mailto:")):
return html.escape(href)
page, _, frag = href.partition("#")
if page.endswith(".md"):
page = page[:-3] + ".html"
return html.escape(page + ("#" + frag if frag else ""))
def slug(text):
"""A heading's anchor: what it says, lowercased, punctuation dropped."""
text = re.sub(r"`|\*\*|\*", "", text)
text = re.sub(r"[^\w\s-]", "", text).strip().lower()
return re.sub(r"[\s_]+", "-", text)
def render(md):
"""Markdown to (html, title, [(level, text, anchor)] for the page's own
table of contents)."""
out, toc, title = [], [], None
lines = md.split("\n")
i = 0
def close(stack):
while stack:
out.append("%s>" % stack.pop())
stack = [] # open list elements, innermost last
while i < len(lines):
line = lines[i]
if line.startswith("```"):
lang = line[3:].strip()
i += 1
body = []
while i < len(lines) and not lines[i].startswith("```"):
body.append(lines[i])
i += 1
i += 1
close(stack)
text = "\n".join(body)
# A block of terminal art rather than a block of code: it is drawn
# with box characters, so it needs leading tight enough for the
# verticals to join and a font whose box drawing does. Detected
# rather than declared, because the alternative is a marker in every
# fence and one of them getting forgotten.
art = not lang and any(c in text for c in "─│╭╮╰╯├┤┬┴┼")
cls = (
' class="art"'
if art
else (' class="lang-%s"' % html.escape(lang) if lang else "")
)
out.append("
%s" % (cls, html.escape(text)))
continue
if re.match(r"^#{1,4} ", line):
close(stack)
level = len(line) - len(line.lstrip("#"))
text = line[level:].strip()
anchor = slug(text)
if level == 1 and title is None:
title = re.sub(r"`", "", text)
else:
toc.append((level, re.sub(r"`", "", text), anchor))
out.append(
'%s" % inline(" ".join(body))) continue if re.match(r"^---+$", line): close(stack) out.append("
%s
" % inline(" ".join(body))) i += 1 close(stack) return "\n".join(out), title or "slosh", toc # ---- the site ------------------------------------------------------------- def read_nav(path): """docs/nav: sections in column zero, pages indented under them.""" sections = [] for raw in open(path): line = raw.rstrip() if not line.strip() or line.lstrip().startswith("#"): continue if line[0].isspace(): if not sections: continue sections[-1][1].append(line.strip()) else: sections.append((line.strip(), [])) return sections def sidebar(sections, pages, current): out = ['") return "\n".join(out) def page_toc(toc): """On this page: the h2s, and the h3s under them.""" items = [(lvl, text, anchor) for lvl, text, anchor in toc if lvl in (2, 3)] if len(items) < 2: return "" out = ['") return "\n".join(out) SHELL = """