320 lines
12 KiB
Python
320 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' - {"μ.twoha.cc" if minimal else "u.twoha.cc"}')
|
|
if not minimal:
|
|
dump = json.dumps(post.metadata)
|
|
assert '-->' not in dump
|
|
content = f'<!--{dump}-->\n{post.content}'
|
|
sheets = (f'<link href="{x}" rel="stylesheet">' 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'<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'])}
|
|
sheets: ["/static/highlight.css"]
|
|
---
|
|
<div class="container">
|
|
<article class="panel">
|
|
<h1>{input['title']}</h1>
|
|
<p>{input['date']}</p>
|
|
<a href=".">back</a>py{{{{ ' | ' if minimal else ' ' }}}}<a href="{path.name}">raw</a>
|
|
<p>{format_tags(input['tags'])}</p>
|
|
<hr>
|
|
{html}
|
|
</article>
|
|
</div>'''
|
|
|
|
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}'
|
|
---
|
|
<div class="container">
|
|
<div class="panel">
|
|
<h2>{dir}</h2>
|
|
<a href="..">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)
|