u.twoha.cc/build.py
2024-09-13 19:49:18 -05:00

309 lines
12 KiB
Python

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 path_write(path, text):
path.parent.mkdir(exist_ok=True, parents=True)
if minimal and path.suffix == '.html':
from bs4 import BeautifulSoup
import minify_html
soup = BeautifulSoup(text, '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()
if tab:
tab = soup.new_tag('style')
tab.string = '*{tab-size:4}'
soup.insert(1, tab)
for tag in soup.find_all(True):
tag.attrs = {k: v for k, v in tag.attrs.items() if k != 'class'}
t = minify_html.minify(str(soup))
path.write_text(t)
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' - {"&#x03BC.twoha.cc" if minimal else "u.twoha.cc"}')
path_write(dest, out)
if not minimal:
content = f'<meta name="metadata" content="{html.escape(json.dumps(post.metadata))}">\n{post.content}'
path_write(outdir / '_partial' / dest.relative_to(outdir), content)
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'<code>{t}</code>' for t in tags)
return " ".join(tags)
def md_to_html(path):
import markdown
from markdown.extensions.codehilite import CodeHiliteExtension
from markdown.extensions.tables import TableExtension
import frontmatter
input = frontmatter.load(path)
if minimal:
ext = ['fenced_code', 'toc', TableExtension()]
else:
ext = ['fenced_code', CodeHiliteExtension(css_class='hl'), 'toc', TableExtension()]
if 'toc' in input:
input.content = '[TOC]\n' + input.content
html = markdown.markdown(input.content, extensions=ext)
return f'''---
title: {repr(input['title'])}
---
<div class="container">
<article class="panel">
<h1>{input['title']}</h1>
<p>{input['date']}</p>
<a href="{'/' / path.parent}">back</a>py{{{{ ' | ' if minimal else ' ' }}}}<a href="{'/' / path}">raw</a>
<p>{format_tags(input['tags'])}</p>
<hr>
{html}
</article>
</div>'''
def index_dir(dir):
import frontmatter
dir = Path(dir)
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.suffix in ['.md', '.html']:
return x.with_suffix('')
return x
def title(x):
if x.suffix in ['.md', '.html']:
return frontmatter.load(x).metadata['title']
return x.name
h = f'''---
title: '{dir}'
---
<div class="container">
<div class="panel">
<h2>{dir}</h2>
<a href="{'/' / dir.parent}">back</a>
<ul>
{chr(10).join(f'<li><a href="/{href(x)}">{title(x)}</a></li>' for x in f)}
</ul>
</div>
</div>'''
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: "", # 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)