--- /dev/null
+.idea/
+__pycache__/
--- /dev/null
+# Home
+
+Hi, I'm mar77i, and I'm from Switzerland.
+
+For blog posts, see under [Blog](blog/)...
--- /dev/null
+#!/usr/bin/env python3
+
+import shutil
+from argparse import ArgumentParser
+from pathlib import Path
+
+from md import MDRenderer
+from template import Template
+from utils import get_content, write_content
+
+
+def main():
+ ap = ArgumentParser()
+ ap.add_argument("--output-dir", default="/dev/shm/output")
+ args = ap.parse_args()
+ source_path = Path(__file__).parent
+ content_path = source_path / "content"
+ output_path = Path(args.output_dir)
+ if output_path.exists():
+ shutil.rmtree(output_path)
+ output_path.mkdir(0o755)
+ template = Template(source_path / "template.html")
+ context = {
+ "nav-items": [
+ {
+ "link": "/",
+ "text": "Home",
+ },
+ {
+ "link": "/blog/",
+ "text": "Blog",
+ },
+ {
+ "link": "https://git.mar77i.info/",
+ "text": "Git",
+ },
+ ]
+ }
+ context["title"], context["page"] = MDRenderer(
+ get_content(content_path / "index.md")
+ ).render_html()
+ write_content(output_path / "index.html", template.render(context))
+
+ #context["blogs"] = []
+ #context["hashtags"] = []
+ #for file in (content_path / "blog").iterdir():
+ # if file.name == "index.md" or not file.name.endswith(".md"):
+ # continue
+ # context["blogs"].append(f"blog/{file.name[:-3]}.html")
+ # context["title"], context["page"] = MDRenderer(get_content(file)).render_html()
+ # write_content(
+ # blog_path / f"{file.name[:-3]}.html", template.render(context),
+ # )
+ #context["title"], context["page"] = MDRenderer(
+ # get_content(content_path / "blog" / "index.md")
+ #).render_html()
+
+ blog_path = output_path / "blog"
+ blog_path.mkdir(0o755)
+ context["title"], context["page"] = "Blog stub", "<h2>Blog stub</h2>"
+ write_content(blog_path / "index.html", template.render(context))
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
--- /dev/null
+from collections import OrderedDict
+from html import escape
+from io import SEEK_END, StringIO
+
+
+class Paragraph:
+ name = "p"
+
+ def __init__(self):
+ self.lines = []
+ self.attributes = OrderedDict()
+
+ def join_attrs(self):
+ if not self.attributes:
+ return ""
+ return "".join(
+ f' {key}="{escape(value)}"' for key, value in self.attributes.items()
+ )
+
+ def join_lines(self):
+ content = '\n'.join(self.lines)
+ sio = StringIO()
+ backslash = False
+ link = None
+ for i, c in enumerate(content):
+ if backslash:
+ assert c in "\\()[]`*/_\n"
+ if c == "\n":
+ c = " "
+ sio.write(c)
+ elif link is not None:
+ if c == "[]()"[len(link)]:
+ link.append(sio.tell())
+ if len(link) == 4:
+ sio.seek(link[0])
+ text = sio.read(link[1] - link[0])
+ sio.read(1 + link[2] - link[1])
+ url = sio.read()
+ sio.seek(link[0])
+ link = None
+ sio.truncate()
+ sio.write('<a href="')
+ sio.write(escape(url.strip()))
+ sio.write('">')
+ sio.write(escape(text, False))
+ sio.write("</a>")
+ continue
+ if c == "\\":
+ backslash = True
+ continue
+ elif c == "[":
+ link = [sio.tell()]
+ continue
+ sio.write(c)
+ assert backslash is False and link is None
+ return sio.getvalue()
+
+ def join(self):
+ return f"<{self.name}{self.join_attrs()}>{self.join_lines()}</{self.name}>"
+
+
+class Heading2(Paragraph):
+ name = "h2"
+
+
+class BulletList(Paragraph):
+ name = "ul"
+
+ class ListItem(Paragraph):
+ name = "li"
+
+ def __init__(self):
+ self.list_items = []
+ super().__init__()
+
+ @property
+ def lines(self):
+ return
+
+ @lines.setter
+ def lines(self, value):
+ self.list_items.append(self.ListItem())
+
+
+class MDRenderer:
+ """
+ Simplified markdown to html translator.
+ """
+ METACHARS = "\\[*/_~`"
+ INLINE_TAGS = {
+ "*": "b",
+ "/": "i",
+ "_": "u",
+ "~": "del",
+ "`": "code",
+ }
+
+ def __init__(self, page):
+ self.page = page
+ self.tag = None
+ self.sio = StringIO()
+
+ def render_html(self):
+ # let's initially support #/<h2>, <p>, and single level lists <ul><li>
+ # where two spaces after a bullet line continues the list item.
+ # backslash escapes transform newlines to spaces in the output
+ # inline tags are evaluated when a tag is closed,
+ # and the in-between text is then html escaped
+ #
+ # return title, html
+ tags = []
+ tag = None
+ title = None
+ for line in self.page.split("\n"):
+ if line.startswith("# "):
+ if not isinstance(tag, Heading2):
+ tag = Heading2()
+ if title is None:
+ title = tag
+ tags.append(tag)
+ line = line[2:]
+ elif line.startswith("- "):
+ if not isinstance(tag, BulletList):
+ tag = BulletList()
+ tags.append(tag)
+ else:
+ tag.lines = []
+ line = line[2:]
+ elif line.startswith(" "):
+ assert isinstance(tag, BulletList)
+ line = line[2:]
+ elif line == "":
+ tag = None
+ continue
+ else:
+ if not isinstance(tag, Paragraph):
+ tag = Paragraph()
+ tags.append(tag)
+ tag.lines.append(line)
+ return title.join_lines(), "\n".join(t.join() for t in tags)
--- /dev/null
+#!/usr/bin/env bash
+
+read -r -d '' hook <<'EOF'
+#!/usr/bin/env bash
+while read _ _ refname; do
+ if [[ "${refname}" == refs/heads/master ]]; then
+ . <(git show master:post-receive.sh)
+ break
+ fi
+done
+EOF
+
+hook_hash="$(printf '%s' "${hook}"|sha256sum| cut -d\ -f1)"
+[[ -f "${dest}" ]] && current_hash="$(sha256sum hooks/post-receive| cut -d\ -f1)"
+
+print_and_run() {
+ printf ">>>"
+ for (( i = 1; i < $# + 1; i++ )); do
+ printf ' %q' "${!i}"
+ done
+ printf "\n"
+ "$@"
+}
+
+update_post_receive_hook() {
+ local stat_cmd="\$(stat -c %a hooks/post-receive)"
+ print_and_run bash -c "cat >hooks/post-receive <<'EOF'"$'\n'"${hook}"$'\nEOF\n'
+ print_and_run bash -c "[[ ${stat_cmd} == 755 ]] || chmod 755 hooks/post-receive"
+}
+
+if [[ "${1}" == --install ]]; then
+ if (( $# > 1 )); then
+ echo "Error: No further arguments expected." >&2
+ exit 1
+ fi
+ update_post_receive_hook
+ exit
+fi
+
+[[ "${hook_hash}" != "${current_hash}" ]] && update_post_receive_hook
+
+git_dir="$(realpath -Pe .)"
+generate_dir=/dev/shm/mar77i.info
+[[ -e "${generate_dir}" ]] && rm -rf "${generate_dir}"
+mkdir "${generate_dir}"
+git --work-tree="${generate_dir}" --git-dir="${git_dir}" checkout master -f
+cd "${generate_dir}"
+./generate.py --output-dir "${HOME}/webroot/www.mar77i.info"
+cd ..
+rm -rf "${generate_dir}"
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>mar77i.info ¬ {title}</title>
+ <link rel="stylesheet" href="/style.css">
+ </head>
+ <body>
+ <h1>mar77i.info</h1>
+ <nav>
+ <ul>
+ {start-nav-items}
+ <li><a href="{link|attrquote}">{text}</a></li>
+ {end-nav-items}
+ </ul>
+ </nav>
+ {page|safe}
+ </body>
+</html>
--- /dev/null
+from collections.abc import Sequence
+
+from html import escape
+from io import StringIO
+
+from utils import find_or_end, get_content
+
+
+class Template:
+ def __init__(self, path):
+ self.path = path
+ self.tokens = self.get_tokens(get_content(path))
+
+ def get_tokens(self, content):
+ """
+ {{ -> literal {
+ }} -> literal }
+ {tag} -> html-escaped tag
+ {tag|safe} -> unescaped tag
+ """
+ tokens = []
+ tokens_stack = [(None, tokens)]
+ current = tokens
+ len_content = len(content)
+ pos = 0
+ while True:
+ new_pos = min(find_or_end(content, c, pos) for c in ("{", "}"))
+ if new_pos - pos > 0:
+ current.append({"text": content[pos:new_pos]})
+ if new_pos == len_content:
+ break
+ pos = new_pos
+ if content[pos] == "}" and (
+ pos == len_content - 1 or content[pos + 1] != "}"
+ ):
+ line = content[:pos].count("\n") + 1
+ raise ValueError(
+ f"Found '}}' without previous '{{': {pos} (line {line})"
+ )
+ if content[pos + 1] in ("{", "}"):
+ if len(current) > 0 and set(current[-1]) == {"text"}:
+ current[-1]["text"] = f"{current[-1]['text']}{content[pos]}"
+ else:
+ current.append({"text": content[pos]})
+ continue
+ closing_tag = content.find("}", pos)
+ if closing_tag == -1:
+ line = content[:pos].count("\n") + 1
+ raise ValueError(
+ f"Found '{{' without subsequent '}}': {pos} (line {line})"
+ )
+ tag, *pipes = content[pos + 1:closing_tag].split("|")
+ pos = closing_tag + 1
+ if tag.startswith("start-"):
+ tag_dict = {
+ "tag": tag[6:],
+ "pipes": pipes,
+ "children": [],
+ }
+ current.append(tag_dict)
+ current = tag_dict["children"]
+ tokens_stack.append((tag[6:], current))
+ continue
+ elif not tag.startswith("end-"):
+ current.append({"tag": tag, "pipes": pipes})
+ continue
+ elif pipes:
+ raise ValueError(f"Unexpected pipes: '{tag}' | {', '.join(pipes)}")
+ elif tag[4:] != tokens_stack.pop()[0]:
+ raise ValueError(f"Mismatched closing tag: '{tag}'")
+ current = tokens_stack[-1][1]
+ if len(tokens_stack) > 1:
+ raise ValueError(f"Found unclosed tag: 'start-{tokens_stack[-1][0]}'")
+ return tokens
+
+ @staticmethod
+ def run_pipes(value, pipes):
+ if pipes == []:
+ value = escape(value, False)
+ else:
+ for pipe in pipes:
+ if pipe == "attrquote":
+ value = escape(value)
+ elif pipe == "safe":
+ pass
+ else:
+ raise ValueError(f"Invalid pipe: '{pipe}'")
+ return value
+
+ def render(self, context, tokens=None):
+ sio = StringIO()
+ for token in self.tokens if tokens is None else tokens:
+ if set(token) == {"text"}:
+ sio.write(token["text"])
+ elif "children" not in token:
+ sio.write(self.run_pipes(context[token["tag"]], token["pipes"]))
+ elif isinstance(token["tag"], Sequence):
+ for ctx in context[token["tag"]]:
+ sio.write(self.render(ctx, token["children"]))
+ return sio.getvalue()
--- /dev/null
+def get_content(path):
+ with path.open("rt") as fh:
+ return fh.read()
+
+
+def write_content(path, content):
+ print(f"writing {path}")
+ with path.open("wt") as fh:
+ fh.write(content)
+
+
+def find_or_end(s, sub, pos=0):
+ pos = s.find(sub, pos)
+ return pos if pos >= 0 else len(s)