author | Alan Dipert
<alan@dipert.org> 2025-10-08 22:55:47 UTC |
committer | Alan Dipert
<alan@dipert.org> 2025-10-08 22:55:47 UTC |
parent | 541ee1418257d65031af89bd5d640d26009eac02 |
.gitignore | +1 | -0 |
Makefile | +8 | -11 |
tools/build_page.py | +104 | -0 |
tools/build_page.sh | +0 | -91 |
tools/buildinfo.py | +112 | -0 |
tools/buildinfo.sh | +0 | -35 |
tools/gen_index.py | +126 | -0 |
tools/gen_index.sh | +0 | -42 |
tools/mdlink2html.awk | +0 | -47 |
tools/mdlink2html.py | +71 | -0 |
tpl/foot.html | +3 | -1 |
tpl/style.css | +12 | -0 |
diff --git a/.gitignore b/.gitignore index 61351e0..8126d18 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ tramp .\#* /out/ +__pycache__/ diff --git a/Makefile b/Makefile index 9e7c942..647dc2f 100644 --- a/Makefile +++ b/Makefile @@ -7,10 +7,10 @@ FOOT := tpl/foot.html CSS := tpl/style.css MD2HTML ?= /usr/bin/cmark-gfm CMARK_FLAGS := --to html --extension table --validate-utf8 --unsafe -BUILDINFO := tools/buildinfo.sh -BUILDINFO_STAMP := $(OUT)/.buildinfo -GIT_HEAD_REF := $(strip $(shell git symbolic-ref -q HEAD 2>/dev/null)) -GIT_HEAD_FILE := $(if $(GIT_HEAD_REF),.git/$(GIT_HEAD_REF),.git/HEAD) +PYTHON ?= python3 +BUILDINFO := tools/buildinfo.py +BUILD_PAGE := tools/build_page.py +GEN_INDEX := tools/gen_index.py INDEX_HTML := $(OUT)/Index.html DEPLOY_HOST ?= arsien23i2@dreamhost:tailrecursion.com/~alan @@ -19,9 +19,6 @@ HTML := $(patsubst $(SRC)/%.md,$(OUT)/%.html,$(MD_FILES)) all: assets $(OUT)/style.css $(HTML) $(INDEX_HTML) -$(BUILDINFO_STAMP): $(BUILDINFO) .git/HEAD $(GIT_HEAD_FILE) | $(OUT) - $(BUILDINFO) > $@ - $(OUT)/style.css: $(CSS) mkdir -p $(OUT) cp $(CSS) $@ @@ -29,16 +26,16 @@ $(OUT)/style.css: $(CSS) $(OUT): mkdir -p $(OUT) -$(OUT)/%.html: $(SRC)/%.md $(HEAD) $(FOOT) tools/mdlink2html.awk tools/build_page.sh $(BUILDINFO_STAMP) | $(OUT) +$(OUT)/%.html: $(SRC)/%.md $(HEAD) $(FOOT) tools/mdlink2html.py $(BUILD_PAGE) $(BUILDINFO) | $(OUT) OUT_DIR="$(OUT)" MD2HTML="$(MD2HTML)" CMARK_FLAGS="$(CMARK_FLAGS)" \ - tools/build_page.sh "$@" "$<" "$(HEAD)" "$(FOOT)" "$(BUILDINFO_STAMP)" tools/mdlink2html.awk + $(PYTHON) $(BUILD_PAGE) "$@" "$<" "$(HEAD)" "$(FOOT)" tools/mdlink2html.py assets: mkdir -p $(OUT) rsync -a --include='*/' --exclude='*.md' --exclude='*.MD' --prune-empty-dirs $(SRC)/ $(OUT)/ -$(INDEX_HTML): $(HTML) tools/gen_index.sh tools/index_list.awk $(HEAD) $(FOOT) $(BUILDINFO_STAMP) | $(OUT) - tools/gen_index.sh $@ $(SRC) $(BUILDINFO_STAMP) +$(INDEX_HTML): $(HTML) $(GEN_INDEX) $(HEAD) $(FOOT) $(BUILDINFO) | $(OUT) + $(PYTHON) $(GEN_INDEX) $@ $(SRC) deploy: check-git-clean assets all @if [ -z "$(DEPLOY_HOST)" ]; then \ diff --git a/tools/build_page.py b/tools/build_page.py new file mode 100755 index 0000000..4eefa00 --- /dev/null +++ b/tools/build_page.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Render a Markdown file into HTML using project templates.""" + +from __future__ import annotations + +import argparse +import html +import os +import shlex +import subprocess +import sys +from pathlib import Path +from typing import List + +TOOLS_DIR = Path(__file__).resolve().parent +if str(TOOLS_DIR) not in sys.path: + sys.path.insert(0, str(TOOLS_DIR)) + +import buildinfo # noqa: E402 +import mdlink2html # noqa: E402 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Render Markdown to HTML") + parser.add_argument("output") + parser.add_argument("input_md") + parser.add_argument("head_tpl") + parser.add_argument("foot_tpl") + parser.add_argument( + "mdlink_script", + nargs="?", + default=str(TOOLS_DIR / "mdlink2html.py"), + help="Path to the Markdown link rewriter script (informational)", + ) + return parser.parse_args() + + +def run_cmark(md_content: str, md2html: str, cmark_flags: List[str]) -> str: + result = subprocess.run( + [md2html, *cmark_flags], + input=md_content, + text=True, + stdout=subprocess.PIPE, + check=True, + ) + return result.stdout + + +def compute_root_prefix(output_path: Path, out_dir: Path) -> str: + try: + relative_path = output_path.relative_to(out_dir) + except ValueError: + return "" + + relative_dir = relative_path.parent + if not relative_dir or relative_dir == Path('.'): + return "" + + depth = len([part for part in relative_dir.parts if part]) + return "../" * depth + + +def render_template(template_path: Path, root_prefix: str, page_title: str, build_info_html: str) -> str: + template = template_path.read_text() + rendered = template.replace("@ROOT@", root_prefix) + rendered = rendered.replace("@TITLE@", html.escape(page_title)) + return rendered.replace("@BUILDINFO@", build_info_html) + + +def main() -> None: + args = parse_args() + + output_path = Path(args.output) + input_md = Path(args.input_md) + head_tpl = Path(args.head_tpl) + foot_tpl = Path(args.foot_tpl) + mdlink_script = Path(args.mdlink_script) + if not mdlink_script.exists(): + raise FileNotFoundError(f"Markdown link rewriter not found: {mdlink_script}") + + md2html = os.environ.get("MD2HTML", "/usr/bin/cmark-gfm") + cmark_flags = shlex.split(os.environ.get("CMARK_FLAGS", "--to html --extension table --validate-utf8 --unsafe")) + out_dir = Path(os.environ.get("OUT_DIR", "out")) + + output_path.parent.mkdir(parents=True, exist_ok=True) + + rewritten_md = mdlink2html.rewrite_file(input_md) + body_html = run_cmark(rewritten_md, md2html, cmark_flags) + + page_title = input_md.stem + build_info_html = buildinfo.build_info(input_md.as_posix()) + root_prefix = compute_root_prefix(output_path, out_dir) + + head_html = render_template(head_tpl, root_prefix, page_title, build_info_html) + foot_html = render_template(foot_tpl, root_prefix, page_title, build_info_html) + + with output_path.open("w") as outf: + outf.write(head_html) + outf.write(body_html) + outf.write(foot_html) + + +if __name__ == "__main__": + main() diff --git a/tools/build_page.sh b/tools/build_page.sh deleted file mode 100755 index ff662a4..0000000 --- a/tools/build_page.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat <<'USAGE' -Usage: build_page.sh OUTPUT INPUT HEAD FOOT BUILDINFO MDLINK_AWK - -Render a Markdown INPUT file into OUTPUT HTML using provided HEAD and FOOT templates. - -Environment: - MD2HTML Path to the cmark-gfm executable (default: /usr/bin/cmark-gfm) - CMARK_FLAGS Flags passed to cmark-gfm (default matches Makefile) - OUT_DIR Root output directory used to compute relative links (default: out) -USAGE -} - -if [[ $# -ne 6 ]]; then - usage >&2 - exit 1 -fi - -output=$1 -input_md=$2 -head_tpl=$3 -foot_tpl=$4 -buildinfo_stamp=$5 -mdlink_awk=$6 - -MD2HTML=${MD2HTML:-/usr/bin/cmark-gfm} -CMARK_FLAGS=${CMARK_FLAGS:---to html --extension table --validate-utf8 --unsafe} -OUT_DIR=${OUT_DIR:-out} - -tmpdir=$(mktemp -d) -cleanup() { - rm -rf "$tmpdir" -} -trap cleanup EXIT INT TERM - -mkdir -p "$(dirname "$output")" - -rewritten_md="$tmpdir/rewritten.md" -body_html="$tmpdir/body.html" -head_html="$tmpdir/head.html" -foot_html="$tmpdir/foot.html" - -awk -f "$mdlink_awk" "$input_md" > "$rewritten_md" - -# shellcheck disable=SC2206 # we intentionally split CMARK_FLAGS into array words -read -r -a cmark_args <<< "$CMARK_FLAGS" -"$MD2HTML" "${cmark_args[@]}" "$rewritten_md" > "$body_html" - -page_title=$(basename "$input_md" .md) -build_info=$(cat "$buildinfo_stamp") - -escape_sed() { - printf '%s\n' "$1" | sed 's/[\\/&]/\\&/g' -} - -build_info_esc=$(escape_sed "$build_info") -page_title_esc=$(escape_sed "$page_title") - -relative_path="${output#$OUT_DIR}" -relative_path="${relative_path#/}" -if [[ "$relative_path" == "$output" ]]; then - relative_path="" -fi -relative_dir="$relative_path" -if [[ -n "$relative_dir" ]]; then - relative_dir=${relative_dir%/*} - if [[ "$relative_dir" == "$relative_path" ]]; then - relative_dir="" - fi -fi - -root_prefix="" -if [[ -n "$relative_dir" && "$relative_dir" != "." ]]; then - IFS='/' read -r -a segments <<< "$relative_dir" - for _ in "${segments[@]}"; do - root_prefix+="../" - done -fi - -sed -e "s|@ROOT@|$root_prefix|g" \ - -e "s|@BUILDINFO@|$build_info_esc|g" \ - -e "s|@TITLE@|$page_title_esc|g" "$head_tpl" > "$head_html" - -sed -e "s|@ROOT@|$root_prefix|g" \ - -e "s|@BUILDINFO@|$build_info_esc|g" \ - -e "s|@TITLE@|$page_title_esc|g" "$foot_tpl" > "$foot_html" - -cat "$head_html" "$body_html" "$foot_html" > "$output" diff --git a/tools/buildinfo.py b/tools/buildinfo.py new file mode 100755 index 0000000..f4fca87 --- /dev/null +++ b/tools/buildinfo.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Generate build metadata for site footers.""" + +from __future__ import annotations + +import argparse +import getpass +import html +import os +import socket +import subprocess +from datetime import datetime +from pathlib import Path +from typing import Optional + + +def ordinal_suffix(day: int) -> str: + if 11 <= day % 100 <= 13: + return "th" + return {1: "st", 2: "nd", 3: "rd"}.get(day % 10, "th") + + +def format_month_day_year(dt: datetime) -> str: + return f"{dt.strftime('%B')} {dt.day}{ordinal_suffix(dt.day)} {dt.year}" + + +def run_git(args: list[str], cwd: Path, check: bool = True) -> str: + result = subprocess.run( + ["git", *args], + cwd=cwd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if check and result.returncode != 0: + raise subprocess.CalledProcessError(result.returncode, result.args, result.stdout, result.stderr) + return result.stdout.strip() + + +def resolve_repo_root(start_dir: Path) -> Path: + output = run_git(["rev-parse", "--show-toplevel"], cwd=start_dir) + if not output: + raise RuntimeError("Unable to locate git repository root") + return Path(output) + + +def resolve_commit(repo_root: Path, target: Optional[str]) -> str: + if target: + rel = Path(target) + if not rel.is_absolute(): + rel = (repo_root / rel).resolve() + try: + rel = rel.relative_to(repo_root) + except ValueError: + rel = Path(target) + commit = run_git(["log", "-n1", "--format=%H", "--", rel.as_posix()], cwd=repo_root, check=False) + if commit: + return commit + return run_git(["rev-parse", "HEAD"], cwd=repo_root) + + +def build_info(target: Optional[str] = None) -> str: + script_dir = Path(__file__).resolve().parent + repo_root = resolve_repo_root(script_dir) + + commit_sha = resolve_commit(repo_root, target) + if not commit_sha: + raise RuntimeError("Failed to resolve commit") + + short_sha = run_git(["rev-parse", "--short", commit_sha], cwd=repo_root) + commit_iso = run_git(["show", "-s", "--format=%cI", commit_sha], cwd=repo_root) + commit_subject = run_git(["show", "-s", "--format=%s", commit_sha], cwd=repo_root) + + commit_dt = datetime.fromisoformat(commit_iso) + commit_date_str = format_month_day_year(commit_dt) + + user = getpass.getuser() + host = socket.gethostname() + build_dt = datetime.now().astimezone() + build_date_str = format_month_day_year(build_dt) + build_time_str = build_dt.strftime("%I:%M %p %Z").lstrip("0") + + commit_url = f"https://tailrecursion.com/git-arr/r/homepage.git/c/{commit_sha}/" + commit_msg_html = html.escape(commit_subject) + + updated_line = ( + f"<p class=\"foot-updated\">Updated {commit_date_str} · " + f"<a href=\"{commit_url}\">{short_sha}</a> — “{commit_msg_html}”</p>" + ) + + built_line = ( + f"<p class=\"foot-built\">Built {build_date_str} at {build_time_str} by {user}@{host}</p>" + ) + + indent = " " + return f"{indent}{updated_line}\n{indent}{built_line}" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Emit build metadata HTML for footers") + parser.add_argument("target", nargs="?", help="Optional path to resolve last commit from") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + print(build_info(args.target)) + + +if __name__ == "__main__": + main() diff --git a/tools/buildinfo.sh b/tools/buildinfo.sh deleted file mode 100755 index 9bac515..0000000 --- a/tools/buildinfo.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ordinal_suffix() { - local day="$1" - case $day in - 11|12|13) printf 'th'; return ;; - esac - case ${day: -1} in - 1) printf 'st' ;; - 2) printf 'nd' ;; - 3) printf 'rd' ;; - *) printf 'th' ;; - esac -} - -main() { - local user host day suffix dow month year time sha_full sha_short repo_url - user=$(whoami) - host=$(hostname) - day=$(date '+%-d') - suffix=$(ordinal_suffix "$day") - dow=$(date '+%A') - month=$(date '+%B') - year=$(date '+%Y') - time=$(date '+%-I:%M%p %Z') - sha_full=$(git rev-parse HEAD) - sha_short=$(git rev-parse --short HEAD) - repo_url="https://tailrecursion.com/git-arr/r/homepage.git/c/${sha_full}/" - - printf 'Built by %s@%s on %s, %s %s%s %s at %s (git <a href="%s">%s</a>)\n' \ - "$user" "$host" "$dow" "$month" "$day" "$suffix" "$year" "$time" "$repo_url" "$sha_short" -} - -main "$@" diff --git a/tools/gen_index.py b/tools/gen_index.py new file mode 100755 index 0000000..ebb2e33 --- /dev/null +++ b/tools/gen_index.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Generate the site index page.""" + +from __future__ import annotations + +import argparse +import html +import os +import sys +from collections import OrderedDict +from pathlib import Path +from typing import Iterable + +TOOLS_DIR = Path(__file__).resolve().parent +if str(TOOLS_DIR) not in sys.path: + sys.path.insert(0, str(TOOLS_DIR)) + +import buildinfo # noqa: E402 + + +class Node: + def __init__(self, name: str) -> None: + self.name = name + self.href: str | None = None + self.children: "OrderedDict[str, Node]" = OrderedDict() + + def child(self, part: str) -> "Node": + if part not in self.children: + self.children[part] = Node(part) + return self.children[part] + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate the index HTML page") + parser.add_argument("output") + parser.add_argument("src_dir", nargs="?", default="md") + parser.add_argument("commit_target", nargs="?", default=None) + return parser.parse_args() + + +def collect_markdown(src_dir: Path) -> Iterable[str]: + return sorted( + (path.relative_to(src_dir).as_posix() for path in src_dir.rglob("*.md")), + key=lambda p: p, + ) + + +def build_tree(paths: Iterable[str]) -> Node: + root = Node("") + for rel_path in paths: + node = root + parts = rel_path[:-3].split("/") # strip .md + for part in parts: + node = node.child(part) + node.href = rel_path + return root + + +def render_children(node: Node, depth: int) -> list[str]: + indent = " " * depth + lines: list[str] = [] + for child in node.children.values(): + display = html.escape(child.name) + if child.href: + href = html.escape("./" + child.href[:-3] + ".html") + line = f"{indent}<li><a href=\"{href}\">{display}</a>" + else: + line = f"{indent}<li>{display}" + + if child.children: + lines.append(line) + lines.append(f"{indent} <ul>") + lines.extend(render_children(child, depth + 1)) + lines.append(f"{indent} </ul>") + lines.append(f"{indent}</li>") + else: + lines.append(f"{line}</li>") + return lines + + +def render_index_list(root: Node) -> str: + lines = ["<ul class=\"site-index\">"] + lines.extend(render_children(root, 0)) + lines.append("</ul>") + return "\n".join(lines) + + +def render_template(template_path: Path, build_info_html: str) -> str: + template = template_path.read_text() + template = template.replace("@ROOT@", "") + template = template.replace("@TITLE@", "Index") + return template.replace("@BUILDINFO@", build_info_html) + + +def main() -> None: + args = parse_args() + + output_path = Path(args.output) + src_dir = Path(args.src_dir) + commit_target = args.commit_target if args.commit_target is not None else args.src_dir + + root_dir = TOOLS_DIR.parent + os.chdir(root_dir) + + build_info_html = buildinfo.build_info(commit_target) + head_html = render_template(root_dir / "tpl" / "head.html", build_info_html) + foot_html = render_template(root_dir / "tpl" / "foot.html", build_info_html) + + body_lines = [ + "<h1>Index of Alan's Homepage</h1>", + "<p>Browse every page in this wiki-style site:</p>", + ] + + markdown_paths = list(collect_markdown(src_dir)) + tree = build_tree(markdown_paths) + body_lines.append(render_index_list(tree)) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with output_path.open("w") as outf: + outf.write(head_html) + outf.write("\n".join(body_lines) + "\n") + outf.write(foot_html) + + +if __name__ == "__main__": + main() diff --git a/tools/gen_index.sh b/tools/gen_index.sh deleted file mode 100755 index 453524e..0000000 --- a/tools/gen_index.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -output=${1:?"usage: gen_index.sh out/Index.html [src-dir] [buildinfo-file]"} -src_dir=${2:-md} -buildinfo_file=${3:-} -script_dir=$(cd "$(dirname "$0")" && pwd) -root_dir=$(cd "$script_dir/.." && pwd) -cd "$root_dir" - -if [[ -n "$buildinfo_file" && -f "$buildinfo_file" ]]; then - build_info=$(<"$buildinfo_file") -else - build_info=$(tools/buildinfo.sh) -fi -build_info_esc=$(printf '%s\n' "$build_info" | sed 's/[\/&]/\\&/g') -css_prefix="" -page_title="Index" - -head_tmp=$(mktemp) -foot_tmp=$(mktemp) -body_tmp=$(mktemp) - -sed -e "s|@ROOT@|$css_prefix|g" \ - -e "s|@BUILDINFO@|$build_info_esc|g" \ - -e "s|@TITLE@|$page_title|g" \ - tpl/head.html > "$head_tmp" - -sed -e "s|@ROOT@|$css_prefix|g" \ - -e "s|@BUILDINFO@|$build_info_esc|g" \ - -e "s|@TITLE@|$page_title|g" \ - tpl/foot.html > "$foot_tmp" - -{ - echo "<h1>Index of Alan's Homepage</h1>" - echo "<p>Browse every page in this wiki-style site:</p>" - find "$src_dir" -type f -name '*.md' | LC_ALL=C sort | awk -v prefix="$src_dir" -f tools/index_list.awk -} > "$body_tmp" - -cat "$head_tmp" "$body_tmp" "$foot_tmp" > "$output" - -rm -f "$head_tmp" "$body_tmp" "$foot_tmp" diff --git a/tools/mdlink2html.awk b/tools/mdlink2html.awk deleted file mode 100755 index 315fa5b..0000000 --- a/tools/mdlink2html.awk +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env awk -f -# Rewrite Markdown links to point at generated HTML pages. - -function rewrite_url(url, path, frag, hash_pos, lower) { - if (url ~ /:\/\//) { - return url - } - if (substr(url, 1, 1) == "#") { - return url - } - - hash_pos = index(url, "#") - if (hash_pos) { - frag = substr(url, hash_pos) - path = substr(url, 1, hash_pos - 1) - } else { - frag = "" - path = url - } - - lower = tolower(path) - if (lower ~ /\.md$/) { - path = substr(path, 1, length(path) - 3) ".html" - } else if (path != "") { - if (lower ~ /\.(html|png|jpg|jpeg|gif|svg|pdf|css|js|zip)$/) { - # leave as-is - } else if (path !~ /\.[A-Za-z0-9]+$/ && path ~ /^[A-Za-z0-9._\-\/]+$/) { - path = path ".html" - } - } - - return path frag -} - -{ - output = "" - remaining = $0 - while (match(remaining, /\]\([^)]+\)/)) { - prefix = substr(remaining, 1, RSTART - 1) - url = substr(remaining, RSTART + 2, RLENGTH - 3) - rewritten = rewrite_url(url) - output = output prefix "](" rewritten ")" - remaining = substr(remaining, RSTART + RLENGTH) - } - output = output remaining - print output -} diff --git a/tools/mdlink2html.py b/tools/mdlink2html.py new file mode 100755 index 0000000..4462e79 --- /dev/null +++ b/tools/mdlink2html.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Rewrite Markdown links to point at generated HTML pages.""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +_LINK_PATTERN = re.compile(r"\]\(([^)]+)\)") +_ALLOWED_PATH_PATTERN = re.compile(r"^[A-Za-z0-9._\-/]+$") +_KNOWN_EXTENSIONS = (".html", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".pdf", ".css", ".js", ".zip") + + +def _rewrite_url(url: str) -> str: + if "://" in url: + return url + if url.startswith("#"): + return url + + frag = "" + path = url + hash_pos = url.find("#") + if hash_pos != -1: + path = url[:hash_pos] + frag = url[hash_pos:] + + lower = path.lower() + if lower.endswith(".md"): + path = path[:-3] + ".html" + elif path: + if lower.endswith(_KNOWN_EXTENSIONS): + pass + elif not re.search(r"\.[A-Za-z0-9]+$", path) and _ALLOWED_PATH_PATTERN.match(path): + path = f"{path}.html" + + return f"{path}{frag}" + + +def rewrite_text(text: str) -> str: + def _replace(match: re.Match[str]) -> str: + url = match.group(1) + rewritten = _rewrite_url(url) + return f"]({rewritten})" + + return _LINK_PATTERN.sub(_replace, text) + + +def rewrite_file(path: Path) -> str: + text = path.read_text() + return rewrite_text(text) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", nargs="?", help="Markdown file to process (defaults to stdin)") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + if args.input: + path = Path(args.input) + sys.stdout.write(rewrite_file(path)) + else: + sys.stdout.write(rewrite_text(sys.stdin.read())) + + +if __name__ == "__main__": + main() diff --git a/tpl/foot.html b/tpl/foot.html index 31e606f..3cd5a90 100644 --- a/tpl/foot.html +++ b/tpl/foot.html @@ -1,7 +1,9 @@ </div> </div> <footer class="site-foot"> - <p>@BUILDINFO@</p> + <div class="foot-meta"> +@BUILDINFO@ + </div> </footer> </div> </body> diff --git a/tpl/style.css b/tpl/style.css index 6d90da2..031184b 100644 --- a/tpl/style.css +++ b/tpl/style.css @@ -86,4 +86,16 @@ img[src*="200px"]{width:200px;} color:var(--muted); font-size:.875rem; } +.site-foot .foot-meta{ + display:flex; + flex-direction:column; + gap:.35rem; +} .site-foot p{margin:0;} +.site-foot a{ + color:inherit; + text-decoration:none; +} +.site-foot a:hover{ + text-decoration:underline; +}