from pathlib import Path from argparse import ArgumentParser import os import re import sys import json def link(src, dst, is_dir=False): dst = Path(dst) if dst.exists(): return dst.parent.mkdir(exist_ok=True, parents=True) dst.symlink_to(Path(src).absolute(), is_dir) def glob_all_to(src, dst): src = Path(src) dst = Path(dst) for page in Path(src).glob('**/*'): if page.name.startswith('.'): continue if page.is_dir(): Path(f'{dst}/{page.relative_to(src)}').mkdir(exist_ok=True, parents=True) else: yield page, Path(f'{dst}/{page.relative_to(src)}') def minify(html): from bs4 import BeautifulSoup import minify_html soup = BeautifulSoup(html, 'html.parser') for div in soup.find_all('div'): div.unwrap() tab = False for code in soup.css.select('pre code'): tab_width = min((x for x in (len(x) - len(x.lstrip(' ')) for x in code.get_text().split('\n')) if x > 1), default=4) new = re.sub(f'^({" " * tab_width})+', lambda x: '\t' * (len(x.group(0)) // tab_width), code.get_text(), flags=re.M) if new != code.string: code.string = new tab = True code.unwrap() sty = soup.new_tag('style') sty.string = '*{tab-size:4;max-width:1000px}' if tab else '*{max-width:1000px}' soup.insert(1, sty) for tag in soup.find_all(True): tag.attrs = {k: v for k, v in tag.attrs.items() if k != 'class'} return minify_html.minify(str(soup)) def path_write(path, text): path.parent.mkdir(exist_ok=True, parents=True) if minimal and path.suffix == '.html': path.write_text(minify(text)) else: path.write_text(text) base = Path('templates/base.html') outdir = Path('/tmp/2h') minimal = False def make_page(page, dest): import html import frontmatter global base assert dest.is_relative_to(outdir) if isinstance(base, Path): base = base.read_text() post = frontmatter.loads(page) post['title'] = post['title'] post.content = re.sub(r"py{{ (.+?) }}", lambda x: str(eval(x.group(1))), post.content) out = base.replace('{{ content }}', post.content).replace('{{ title }}', post['title'] + f' - {"μ.twoha.cc" if minimal else "u.twoha.cc"}') if not minimal: dump = json.dumps(post.metadata) assert '-->' not in dump content = f'\n{post.content}' sheets = (f'' for x in ['/static/style.css', *post.get('sheets', [])]) out = out.replace('{{ sheets }}', '\n'.join(sheets)) path_write(outdir / '_partial' / dest.relative_to(outdir), content) path_write(dest, out) else: path_write(dest, out) def main(_): outdir.mkdir(exist_ok=True, parents=True) link('static', outdir / 'static', True) link('music', outdir / 'music', True) for s in Path('rootstatic').glob('*'): link(f'rootstatic/{s.name}', outdir / s.name) for s, d in glob_all_to('pages', outdir): make_page(s.read_text(), d) def songlist(_): from mutagen.id3 import ID3 p = Path('static/songlist.json') old = set() songs = [] if p.exists(): for song in json.loads(p.read_text()): old.add(song['href']) songs.append(song) for mp3 in Path('music').glob('*.mp3'): i = ID3(mp3) if f'/{mp3}' not in old: songs.append({ 'name': i.get('TIT2')[0], 'artist': (i.get('TCOM') or i.get('TPE1') or [''])[0], 'href': f'/{mp3}', 'src': '' }) songs.sort(key=lambda x: (x['src'], x['artist'], x['name'])) with open('static/songlist.json', 'w') as f: json.dump(songs, f, indent=2, ensure_ascii=False) def format_tags(tags): tags = (f'{t}' for t in tags) return " ".join(tags) def md_to_html(path): import markdown from markdown.extensions.codehilite import CodeHiliteExtension import frontmatter input = frontmatter.load(path) import markdown_katex.extension import markdown_katex.wrapper ext = ['fenced_code', 'toc', 'tables', 'markdown_katex'] markdown_katex.extension.KATEX_STYLES = '' sheets = ['/static/highlight.css'] if minimal: import latex2mathml.converter def convert(t, _): return latex2mathml.converter.convert(t) markdown_katex.wrapper.tex2html = convert else: real = markdown_katex.wrapper.tex2html def convert(*args): sheets.append('/static/katex.min.css') markdown_katex.wrapper.tex2html = real return real(*args) markdown_katex.wrapper.tex2html = convert ext.append(CodeHiliteExtension(css_class='hl')) if 'toc' in input: input.content = '[TOC]\n' + input.content html = markdown.markdown(input.content, extensions=ext) return f'''--- title: {repr(input['title'])} sheets: {repr(sheets)} ---

{input['title']}

{input['date']}

backpy{{{{ ' | ' if minimal else ' ' }}}}raw

{format_tags(input['tags'])}


{html}
''' def index_dir(dir): import frontmatter dir = Path(dir) if dir.name.startswith('.'): return if (dir / '.order').exists(): order = (dir / '.order').read_text().split('\n') f = [p for p in dir.glob('*') if not p.name.startswith('.')] f.sort(key=lambda x: order.index(x.name) if x.name in order else -1) else: f = reversed(sorted([p for p in dir.glob('*') if not p.name.startswith('.')], key=os.path.getmtime)) def href(x): if x.is_dir(): return f'{x.name}/' if x.suffix in ['.md', '.html']: return x.with_suffix('').name return x.name def title(x): if x.suffix in ['.md', '.html']: return frontmatter.load(x).metadata['title'] return x.name h = f'''--- title: '{dir}' ---

{dir}

back
''' make_page(h, outdir / dir / 'index.html') def ctf(args): if args.path is not None: return ctf_page(args.path) for s, d in glob_all_to('ctf', outdir / 'ctf'): html = md_to_html(s) make_page(html, d.with_suffix('.html')) link(s, d) ctfs = list(Path('ctf').glob('*')) for d in ctfs: index_dir(d) index_dir('ctf') def ctf_page(path): if path.name.startswith('.'): return output = (outdir / path).with_suffix('.html') html = md_to_html(path) make_page(html, output) link(path, output.with_suffix('.md')) index_dir(path.parent) index_dir('ctf') def highlight(_): from pygments.style import Style from pygments.token import Token from pygments.formatters import HtmlFormatter class S(Style): background_color = "#272822" highlight_color = "#49483e" styles = { # No corresponding class for the following: Token: "#f8f8f2", # class: '' Token.Whitespace: "", # class: 'w' Token.Error: "#ed007e bg:#1e0010", # class: 'err' Token.Other: "", # class 'x' Token.Comment: "#959077", # class: 'c' Token.Comment.Multiline: "", # class: 'cm' Token.Comment.Preproc: "", # class: 'cp' Token.Comment.Single: "", # class: 'c1' Token.Comment.Special: "", # class: 'cs' Token.Keyword: "#66d9ef", # class: 'k' Token.Keyword.Constant: "", # class: 'kc' Token.Keyword.Declaration: "", # class: 'kd' Token.Keyword.Namespace: "#ff4689", # class: 'kn' Token.Keyword.Pseudo: "", # class: 'kp' Token.Keyword.Reserved: "", # class: 'kr' Token.Keyword.Type: "", # class: 'kt' Token.Operator: "#ff4689", # class: 'o' Token.Operator.Word: "", # class: 'ow' - like keywords Token.Punctuation: "#f8f8f2", # class: 'p' Token.Name: "#f8f8f2", # class: 'n' Token.Name.Attribute: "#a6e22e", # class: 'na' - to be revised Token.Name.Builtin: "#66d9ef", # class: 'nb' Token.Name.Builtin.Pseudo: "", # class: 'bp' Token.Name.Class: "#a6e22e", # class: 'nc' - to be revised Token.Name.Constant: "#66d9ef", # class: 'no' - to be revised Token.Name.Decorator: "#a6e22e", # class: 'nd' - to be revised Token.Name.Entity: "", # class: 'ni' Token.Name.Exception: "#a6e22e", # class: 'ne' Token.Name.Function: "#a6e22e", # class: 'nf' Token.Name.Property: "", # class: 'py' Token.Name.Label: "", # class: 'nl' Token.Name.Namespace: "", # class: 'nn' - to be revised Token.Name.Other: "#a6e22e", # class: 'nx' Token.Name.Tag: "#ff4689", # class: 'nt' - like a keyword Token.Name.Variable: "", # class: 'nv' - to be revised Token.Name.Variable.Class: "", # class: 'vc' - to be revised Token.Name.Variable.Global: "", # class: 'vg' - to be revised Token.Name.Variable.Instance: "", # class: 'vi' - to be revised Token.Number: "#ae81ff", # class: 'm' Token.Number.Float: "", # class: 'mf' Token.Number.Hex: "", # class: 'mh' Token.Number.Integer: "", # class: 'mi' Token.Number.Integer.Long: "", # class: 'il' Token.Number.Oct: "", # class: 'mo' Token.Literal: "#ae81ff", # class: 'l' Token.Literal.Date: "#e6db74", # class: 'ld' Token.String: "#e6db74", # class: 's' Token.String.Backtick: "", # class: 'sb' Token.String.Char: "", # class: 'sc' Token.String.Doc: "", # class: 'sd' - like a comment Token.String.Double: "", # class: 's2' Token.String.Escape: "#ae81ff", # class: 'se' Token.String.Heredoc: "", # class: 'sh' Token.String.Interpol: "#f8f8f2", # class: 'si' Token.String.Other: "", # class: 'sx' Token.String.Regex: "", # class: 'sr' Token.String.Single: "", # class: 's1' Token.String.Symbol: "", # class: 'ss' Token.Generic: "", # class: 'g' Token.Generic.Deleted: "#ff4689", # class: 'gd', Token.Generic.Emph: "italic", # class: 'ge' Token.Generic.Error: "", # class: 'gr' Token.Generic.Heading: "", # class: 'gh' Token.Generic.Inserted: "#a6e22e", # class: 'gi' Token.Generic.Output: "#e6db74", # class: 'go' Token.Generic.Prompt: "bold #ff4689", # class: 'gp' Token.Generic.Strong: "bold", # class: 'gs' Token.Generic.EmphStrong: "bold italic", # class: 'ges' Token.Generic.Subheading: "#959077", # class: 'gu' Token.Generic.Traceback: "", # class: 'gt' } formatter = HtmlFormatter(style=S) css = formatter.get_style_defs('.hl') Path('static/highlight.css').write_text(css) commands = { 'songlist': songlist, 'ctf': ctf, 'highlight': highlight, 'main': main, } if __name__ == '__main__': parser = ArgumentParser('build') parser.add_argument('--output', '-o', default=outdir, type=Path) parser.add_argument('--path', '-p', type=Path) parser.add_argument('--minimal', '-m', action='store_true') parser.add_argument('command', nargs="?", default='main') args = parser.parse_args() if args.minimal: minimal = True base = Path('templates/minimal.html') outdir = args.output if args.command in commands: commands[args.command](args) else: print(f'no command called {args.command}') exit(-1)