Merge branch 'experiment/blog-generation' into develop

This commit is contained in:
Sergio Martínez Portela 2022-08-19 19:39:19 +02:00
commit 1280de0ff9
12 changed files with 886 additions and 16 deletions

122
scripts/autoserve.py Normal file
View File

@ -0,0 +1,122 @@
#!/usr/bin/env python3
import sys
import http.server
import socketserver
import threading
import os
import time
import select
import inotify.adapters
PORT = 8000
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
MONITORED_EVENT_TYPES = (
'IN_CREATE',
# 'IN_MODIFY',
'IN_CLOSE_WRITE',
'IN_DELETE',
'IN_MOVED_FROM',
'IN_MOVED_TO',
'IN_DELETE_SELF',
'IN_MOVE_SELF',
)
WAITING_RESPONSES = []
SLEEP_TIME = 0.5
COUNTER = 0
MAX_WAITS = 100
class Server(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path.strip('/') == '__wait_for_changes':
WAITING_RESPONSES.append(self)
print(len(WAITING_RESPONSES), "waiting responses")
global COUNTER
ticket, COUNTER = COUNTER, COUNTER + 1
while self in WAITING_RESPONSES:
# This is an horribe way to wait! ... but it may work for quick tests 🤷
if COUNTER - ticket > MAX_WAITS:
# Connection closed by the other side
print("Manually closed for cleanup")
WAITING_RESPONSES.remove(self)
# send 502 response, timeout
self.send_response(502)
# send response headers
self.end_headers()
return
time.sleep(SLEEP_TIME)
return
# send 200 response
self.send_response(200)
# send response headers
self.end_headers()
with open(self.path.strip('/'), 'rb') as f:
# send the body of the response
self.wfile.write(f.read())
if not self.path.endswith('.html'):
return
else:
# Append update waiter
with open(os.path.join(THIS_DIR, 'wait_for_update.js'), 'rb') as f:
new_data = b'<script>' + f.read() + b'</script>'
self.wfile.write(new_data)
new_data_len = len(new_data)
return
def notify_reloads():
while len(WAITING_RESPONSES) > 0:
# Close opened connections
res = WAITING_RESPONSES.pop(0)
try:
# send 200 response
res.send_response(200)
# send response headers
res.end_headers()
except Exception as e:
print("ERROR:", e)
global COUNTER
COUNTER = 0
def start_notifier():
notifier = inotify.adapters.InotifyTree(os.getcwd())
should_reload = False
for event in notifier.event_gen(yield_nones=True):
if event is None:
if should_reload:
print("Reloading!")
should_reload = False
notify_reloads()
continue
(ev, types, directory, file) = event
if any([type in MONITORED_EVENT_TYPES for type in types]):
print("Detected change!", types, directory, file)
should_reload = True
def serve():
Handler = Server
notifier_thread = threading.Thread(target=start_notifier)
with http.server.ThreadingHTTPServer(("127.0.0.1", PORT), Handler) as httpd:
print("serving at port", PORT)
notifier_thread.start()
httpd.serve_forever()
if __name__ == '__main__':
serve()

345
scripts/blog.py Normal file
View File

@ -0,0 +1,345 @@
#!/usr/bin/env python3
MARKDOWN_EXTENSION = '.md'
EXTENSIONS = [
MARKDOWN_EXTENSION,
]
MARKDOWN_EXTRA_FEATURES = [
# See more in: https://python-markdown.github.io/extensions/
'markdown.extensions.fenced_code',
'markdown.extensions.codehilite',
'markdown.extensions.extra',
]
import json
import logging
import sys
import os
import datetime
import shutil
import traceback
import time
import re
from typing import List
from bs4 import BeautifulSoup as bs4
import jinja2
import inotify.adapters
import yaml
import markdown
from unidecode import unidecode
NIKOLA_DATE_RE = re.compile(r'^([0-2]\d|30|31)\.(0\d|1[012])\.(\d{4}), (\d{1,2}):(\d{2})$')
COMPLETE_DATE_RE = re.compile(r'^(\d{4})-(0\d|1[012])-([0-2]\d|30|31) '
+ r'(\d{2}):(\d{2})(:\d{2})( .+)?$')
SLUG_HYPHENATE_RE = re.compile(r'[\s\-]+')
SLUG_REMOVE_RE = re.compile(r'[^\s\-a-zA-Z0-9]*')
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_PATH = os.path.join(ROOT_DIR, 'static')
ARTICLE_TEMPLATE_NAME = 'article.tmpl.html'
BLOG_INDEX_TEMPLATE_NAME = 'blog_index.tmpl.html'
BLOG_INDEX_PAGE_SIZE = 10
STATIC_RESOURCES = (
('style.css', 'css/style.css'),
('light-syntax.css', 'css/light-syntax.css'),
('dark-syntax.css', 'css/dark-syntax.css', ('@media (prefers-color-scheme: dark) {\n', '\n}')),
)
JINJA_ENV = jinja2.Environment(
loader=jinja2.FileSystemLoader(STATIC_PATH),
autoescape=jinja2.select_autoescape()
)
def update_statics():
global ARTICLE_TEMPLATE
ARTICLE_TEMPLATE = JINJA_ENV.get_template(ARTICLE_TEMPLATE_NAME)
global BLOG_INDEX_TEMPLATE
BLOG_INDEX_TEMPLATE = JINJA_ENV.get_template(BLOG_INDEX_TEMPLATE_NAME)
update_statics()
MONITORED_EVENT_TYPES = (
'IN_CREATE',
# 'IN_MODIFY',
'IN_CLOSE_WRITE',
'IN_DELETE',
'IN_MOVED_FROM',
'IN_MOVED_TO',
'IN_DELETE_SELF',
'IN_MOVE_SELF',
)
def parse_nikola_date(match):
return datetime.datetime(year=int(match.group(3)),
month=int(match.group(2)),
day=int(match.group(1)),
hour=int(match.group(4)),
minute=int(match.group(5)),
# Note this final assumption is not good
# and might get you in trouble if trying
# to sort closely-published posts
# when others are in complete-date format
tzinfo=datetime.timezone.utc,
)
def parse_complete_date(match):
return datetime.datetime.strptime(match.group(0), '%Y-%m-%d %H:%M:%S %Z%z')
def split_tags(tags: str) -> List[str]:
if isinstance(tags, str):
return [tag.strip() for tag in tags.split(',')]
elif isinstance(tags, list):
return tags
else:
raise NotImplementedError("Unknown tag type: {}".format(type(tags)))
def slugify(title):
"""
Made for compatibility with Nikola's slugify within CodigoParaLlevar blog.
"""
slug = unidecode(title).lower()
slug = SLUG_REMOVE_RE.sub('', slug)
slug = SLUG_HYPHENATE_RE.sub('-', slug)
return slug.strip()
def read_markdown(path):
with open(path, 'rt') as f:
data = f.read()
if data.startswith('---'):
start = data.index('\n')
if '---\n' not in data[start:]:
raise Exception('Front matter not finished on: {}'.format(path))
front_matter_str, content = data[start:].split('---\n', 1)
front_matter = yaml.load(front_matter_str, Loader=yaml.SafeLoader)
else:
raise Exception('Front matter is needed for proper rendering. Not found on: {}'.format(
path
))
doc = markdown.markdown(content, extensions=MARKDOWN_EXTRA_FEATURES)
return doc, front_matter
def get_out_path(front_matter):
if 'date' in front_matter:
if m := NIKOLA_DATE_RE.match(front_matter['date']):
front_matter['date'] = parse_nikola_date(m)
elif m := COMPLETE_DATE_RE.match(front_matter['date']):
front_matter['date'] = parse_complete_date(m)
else:
raise NotImplementedError('Unknown date format: {}'.format(
front_matter['date']))
else:
raise Exception('No date found on: {}'.format(
path
))
if 'slug' not in front_matter:
if 'title' not in front_matter:
raise Exception('No title found on: {}'.format(
path
))
front_matter['slug'] = slugify(front_matter['title'])
out_path = os.path.join(str(front_matter['date'].year), front_matter['slug'])
return out_path
def load_all(top_dir_relative):
top = os.path.abspath(top_dir_relative)
docs = {}
for root, dirs, files in os.walk(top):
for name in files:
if all([not name.endswith(ext) for ext in EXTENSIONS]):
# The logic is negative... but it works
continue
if name.endswith(MARKDOWN_EXTENSION):
path = os.path.join(root, name)
doc, front_matter = read_markdown(path)
out_path = get_out_path(front_matter)
docs[path] = (doc, front_matter, out_path)
else:
raise NotImplementedError('Unknown filetype: {}'.format(name))
return docs
def load_doc(filepath):
doc, front_matter = read_markdown(filepath)
out_path = get_out_path(front_matter)
return (doc, front_matter, out_path)
def render_article(doc, front_matter, f):
result = ARTICLE_TEMPLATE.render(
content=doc,
title=front_matter['title'],
post_publication_date=front_matter['date'],
post_tags=split_tags(front_matter['tags']),
)
f.write(result)
def summarize(doc):
return bs4(doc, features='lxml').text[:1000]
def render_index(docs, dest_top):
docs = sorted(docs.values(), key=lambda x: x[1]['date'], reverse=True)
for off in range(0, len(docs), BLOG_INDEX_PAGE_SIZE):
page = docs[off: off + BLOG_INDEX_PAGE_SIZE]
posts = [
{
"doc": doc,
"title": front_matter['title'],
"post_publication_date": front_matter['date'],
"post_tags": split_tags(front_matter['tags']),
"summary": summarize(doc),
}
for (doc, front_matter, out_path) in page
]
result = BLOG_INDEX_TEMPLATE.render(
posts=posts,
)
if off == 0:
fname = 'index.html'
else:
fname = 'index-{}.html'.format(off // BLOG_INDEX_PAGE_SIZE)
with open(os.path.join(dest_top, fname), 'wt') as f:
f.write(result)
def regen_all(source_top, dest_top, docs=None):
if docs is None:
docs = load_all(source_top)
# Render posts
for (doc, front_matter, out_path) in docs.values():
doc_full_path = os.path.join(dest_top, out_path)
os.makedirs(os.path.dirname(doc_full_path), exist_ok=True)
# print("==", doc_full_path)
with open(doc_full_path + '.html', 'wt') as f:
try:
render_article(doc, front_matter, f)
except:
logging.error(traceback.format_exc())
logging.error("Rendering failed 😿")
continue
# Render statics
for static in STATIC_RESOURCES:
src_path = static[0]
dest_path = static[1]
if len(static) > 2:
before, after = static[2]
else:
before, after = '', ''
target_dest = os.path.join(dest_top, dest_path)
os.makedirs(os.path.dirname(target_dest), exist_ok=True)
with open(os.path.join(STATIC_PATH, src_path), 'rt') as src:
data = before + src.read() + after
with open(target_dest, 'wt') as f:
f.write(data)
# Render index
render_index(docs, dest_top)
return docs
def main(source_top, dest_top):
notifier = inotify.adapters.InotifyTrees([source_top, STATIC_PATH])
## Initial load
t0 = time.time()
logging.info("Initial load...")
docs = regen_all(source_top, dest_top)
logging.info("Initial load completed in {:.2f}s".format(time.time() - t0))
## Updating
for event in notifier.event_gen(yield_nones=False):
(ev, types, directory, file) = event
if not any([type in MONITORED_EVENT_TYPES for type in types]):
continue
filepath = os.path.join(directory, file)
if filepath.startswith(STATIC_PATH):
t0 = time.time()
try:
update_statics()
except:
logging.error(traceback.format_exc())
logging.error("Loading new templates failed 😿")
continue
is_static_resource = False
for static in STATIC_RESOURCES:
src_path = static[0]
dest_path = static[1]
if file == os.path.basename(src_path):
is_static_resource = True
if len(static) > 2:
before, after = static[2]
else:
before, after = '', ''
target_dest = os.path.join(dest_top, dest_path)
os.makedirs(os.path.dirname(target_dest), exist_ok=True)
with open(os.path.join(STATIC_PATH, src_path), 'rt') as src:
data = before + src.read() + after
with open(target_dest, 'wt') as f:
f.write(data)
if is_static_resource:
logging.info("Updated static resources in {:.2f}s".format(time.time() - t0))
else:
docs = regen_all(source_top, dest_top, docs)
logging.info("Updated all in {:.2f}s".format(time.time() - t0))
else:
try:
(doc, front_matter, out_path) = load_doc(filepath)
except:
logging.error(traceback.format_exc())
logging.error("Skipping update 😿")
continue
t0 = time.time()
docs[filepath] = (doc, front_matter, out_path)
doc_full_path = os.path.join(dest_top, out_path)
os.makedirs(os.path.dirname(doc_full_path), exist_ok=True)
# print("==", doc_full_path)
with open(doc_full_path + '.html', 'wt') as f:
try:
render_article(doc, front_matter, f)
except:
logging.error(traceback.format_exc())
logging.error("Rendering failed 😿")
continue
logging.info("Updated all in {:.2f}s".format(time.time() - t0))
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: {} SOURCE_TOP DEST_TOP".format(sys.argv[0]))
exit(0)
logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
main(sys.argv[1], sys.argv[2])

View File

@ -138,7 +138,7 @@ def main(src_top, dest_top):
elif l.value.startswith('./'):
pass # TODO: Properly handle
else:
raise NotImplementedError('On document {}, link to {}'.format(doc.path, l.value))
logging.warning('On document {}, unknown link to {}'.format(doc.path, l.value))
if headline.parent:
if isinstance(headline.parent, org_rw.Headline):
@ -169,7 +169,7 @@ def main(src_top, dest_top):
json.dump(obj=graph, fp=f, indent=2)
graph_explorer_path = os.path.join(dest_top, "graph.html")
with open(graph_explorer_path, 'wt') as f:
with open(os.path.join(os.path.dirname(os.path.abspath(dest_top)), 'static', 'graph_explorer.html'), 'rt') as template:
with open(os.path.join(os.path.dirname(os.path.abspath(dest_top)), '..', 'static', 'graph_explorer.html'), 'rt') as template:
source = template.read()
f.write(source.replace('<!-- REPLACE_THIS_WITH_GRAPH -->',
json.dumps(graph)))

2
scripts/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Markdown
Jinja2

View File

@ -0,0 +1,19 @@
(function (){
var wait_for_update = function() {
console.debug("Waiting for changes...");
fetch('/__wait_for_changes').then(r => {
if (r.status !== 200) {
setTimeout(
wait_for_update,
1000,
);
}
else {
// Reload
window.location = window.location;
}
});
};
wait_for_update();
})();

48
static/article.tmpl.html Normal file
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ title }} @ Código para llevar</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../css/style.css" />
<link rel="stylesheet" href="../css/light-syntax.css" />
<link rel="stylesheet" href="../css/dark-syntax.css" />
</head>
<body>
<div class="site-header">
<h1 class="site-name"><a href="/">Codigo para llevar</a></h1>
<nav class="site-links">
<span class="fancy-link">
<a href="https://codigoparallevar.com/">Home</a>
</span>
<span class="fancy-link">
<a href="https://github.com/kenkeiras">GitHub</a>
</span>
<span class="fancy-link">
<a href="https://gitlab.com/kenkeiras">GitLab</a>
</span>
<span class="fancy-link">
<a href="https://programaker.com/users/kenkeiras">PrograMaker</a>
</span>
</nav>
</div>
<div class="content">
<article class="post">
<h2 class="post-title">{{ title }}</h2>
<div class="post-metadata">
<time datetime="{{ post_publication_date }}">
{{ post_publication_date }}
</time>
<ul class="post-tags">
{% for post_tag in post_tags %}
<li class="post-tag">{{ post_tag }}</li>
{% endfor %}
</ul>
</div>
<div class="post-content">
{{ content | safe }}
</div>
</article>
</div>
</body>
</html>

6
static/blog.css Normal file
View File

@ -0,0 +1,6 @@
body {
margin: 0 auto;
width: fit-content;
max-width: 100ex;
padding: 0 1ex;
}

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Código para llevar</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="css/light-syntax.css" />
<link rel="stylesheet" href="css/dark-syntax.css" />
</head>
<body>
<div class="site-header">
<h1 class="site-name"><a href="/">Codigo para llevar</a></h1>
<nav class="site-links">
<span class="fancy-link">
<a href="https://codigoparallevar.com/">Home</a>
</span>
<span class="fancy-link">
<a href="https://github.com/kenkeiras">GitHub</a>
</span>
<span class="fancy-link">
<a href="https://gitlab.com/kenkeiras">GitLab</a>
</span>
<span class="fancy-link">
<a href="https://programaker.com/users/kenkeiras">PrograMaker</a>
</span>
</nav>
</div>
<div class="post-index content">
{% for post in posts %}
<div class="post-container">
<article class="post">
<h2 class="post-title">{{ post.title }}</h2>
<div class="post-metadata">
<time datetime="{{ post.post_publication_date }}">
{{ post.post_publication_date }}
</time>
<ul class="post-tags">
{% for post_tag in post.post_tags %}
<li class="post-tag">{{ post_tag }}</li>
{% endfor %}
</ul>
</div>
<div class="post-content">
{{ post.summary | safe }}
</div>
</article>
</div>
{% endfor %}
</div>
</body>
</html>

82
static/dark-syntax.css Normal file
View File

@ -0,0 +1,82 @@
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.hll { background-color: #49483e }
.c { color: #75715e } /* Comment */
.err { color: #960050; background-color: #1e0010 } /* Error */
.esc { color: #f8f8f2 } /* Escape */
.g { color: #f8f8f2 } /* Generic */
.k { color: #66d9ef } /* Keyword */
.l { color: #ae81ff } /* Literal */
.n { color: #f8f8f2 } /* Name */
.o { color: #f92672 } /* Operator */
.x { color: #f8f8f2 } /* Other */
.p { color: #f8f8f2 } /* Punctuation */
.ch { color: #75715e } /* Comment.Hashbang */
.cm { color: #75715e } /* Comment.Multiline */
.cp { color: #75715e } /* Comment.Preproc */
.cpf { color: #75715e } /* Comment.PreprocFile */
.c1 { color: #75715e } /* Comment.Single */
.cs { color: #75715e } /* Comment.Special */
.gd { color: #f92672 } /* Generic.Deleted */
.ge { color: #f8f8f2; font-style: italic } /* Generic.Emph */
.gr { color: #f8f8f2 } /* Generic.Error */
.gh { color: #f8f8f2 } /* Generic.Heading */
.gi { color: #a6e22e } /* Generic.Inserted */
.go { color: #66d9ef } /* Generic.Output */
.gp { color: #f92672; font-weight: bold } /* Generic.Prompt */
.gs { color: #f8f8f2; font-weight: bold } /* Generic.Strong */
.gu { color: #75715e } /* Generic.Subheading */
.gt { color: #f8f8f2 } /* Generic.Traceback */
.kc { color: #66d9ef } /* Keyword.Constant */
.kd { color: #66d9ef } /* Keyword.Declaration */
.kn { color: #f92672 } /* Keyword.Namespace */
.kp { color: #66d9ef } /* Keyword.Pseudo */
.kr { color: #66d9ef } /* Keyword.Reserved */
.kt { color: #66d9ef } /* Keyword.Type */
.ld { color: #e6db74 } /* Literal.Date */
.m { color: #ae81ff } /* Literal.Number */
.s { color: #e6db74 } /* Literal.String */
.na { color: #a6e22e } /* Name.Attribute */
.nb { color: #f8f8f2 } /* Name.Builtin */
.nc { color: #a6e22e } /* Name.Class */
.no { color: #66d9ef } /* Name.Constant */
.nd { color: #a6e22e } /* Name.Decorator */
.ni { color: #f8f8f2 } /* Name.Entity */
.ne { color: #a6e22e } /* Name.Exception */
.nf { color: #a6e22e } /* Name.Function */
.nl { color: #f8f8f2 } /* Name.Label */
.nn { color: #f8f8f2 } /* Name.Namespace */
.nx { color: #a6e22e } /* Name.Other */
.py { color: #f8f8f2 } /* Name.Property */
.nt { color: #f92672 } /* Name.Tag */
.nv { color: #f8f8f2 } /* Name.Variable */
.ow { color: #f92672 } /* Operator.Word */
.w { color: #f8f8f2 } /* Text.Whitespace */
.mb { color: #ae81ff } /* Literal.Number.Bin */
.mf { color: #ae81ff } /* Literal.Number.Float */
.mh { color: #ae81ff } /* Literal.Number.Hex */
.mi { color: #ae81ff } /* Literal.Number.Integer */
.mo { color: #ae81ff } /* Literal.Number.Oct */
.sa { color: #e6db74 } /* Literal.String.Affix */
.sb { color: #e6db74 } /* Literal.String.Backtick */
.sc { color: #e6db74 } /* Literal.String.Char */
.dl { color: #e6db74 } /* Literal.String.Delimiter */
.sd { color: #e6db74 } /* Literal.String.Doc */
.s2 { color: #e6db74 } /* Literal.String.Double */
.se { color: #ae81ff } /* Literal.String.Escape */
.sh { color: #e6db74 } /* Literal.String.Heredoc */
.si { color: #e6db74 } /* Literal.String.Interpol */
.sx { color: #e6db74 } /* Literal.String.Other */
.sr { color: #e6db74 } /* Literal.String.Regex */
.s1 { color: #e6db74 } /* Literal.String.Single */
.ss { color: #e6db74 } /* Literal.String.Symbol */
.bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
.fm { color: #a6e22e } /* Name.Function.Magic */
.vc { color: #f8f8f2 } /* Name.Variable.Class */
.vg { color: #f8f8f2 } /* Name.Variable.Global */
.vi { color: #f8f8f2 } /* Name.Variable.Instance */
.vm { color: #f8f8f2 } /* Name.Variable.Magic */
.il { color: #ae81ff } /* Literal.Number.Integer.Long */

View File

@ -6,11 +6,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: sans-serif;
margin: 0 auto;
width: fit-content;
max-width: 100ex;
padding: 0 1ex;
}
.header h1 {
text-align: center;
}
.links section {
margin-top: 1em;
display: inline-block;
@ -80,14 +84,24 @@
<section>
<h2>Collaborations</h2>
<p>
Latest post in <a href="https://hackliza.gal">Hackliza</a>: <a href="https://hackliza.gal/en/posts/quick_math_on_terminal/">Quick math on the terminal (english)</a>
Latest posts in <a href="https://hackliza.gal">Hackliza</a>:
<ul>
<li>
<a href="https://hackliza.gal/en/posts/python-visual-profiling/">Visual profiling in Python (english)</a>
<a href="https://hackliza.gal/posts/python-visual-profiling/">(galician)</a>
</li>
<li>
<a href="https://hackliza.gal/en/posts/quick_math_on_terminal/">Quick math on the terminal (english)</a>
<a href="https://hackliza.gal/posts/contas_rapidas_no_terminal/">(galician)</a>
</li>
</ul>
</p>
</section>
<section>
<h2>Projects</h2>
<p>
My most stable project is <a href="https://programaker.com">PrograMaker</a>. Other work-in-progress is in <a href="https://github.com/kenkeiras">GitHub</a>.
My most stable project is <a href="https://programaker.com">PrograMaker</a>.
Other work-in-progress is in <a href="https://github.com/kenkeiras">GitHub</a>.
</p>
</section>
<section id="social">

64
static/light-syntax.css Normal file
View File

@ -0,0 +1,64 @@
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.hll { background-color: #ffffcc }
.c { color: #008800; font-style: italic } /* Comment */
.err { color: #a61717; background-color: #e3d2d2 } /* Error */
.g { color: #2c2cff } /* Generic */
.k { color: #2c2cff } /* Keyword */
.x { background-color: #ffffe0 } /* Other */
.ch { color: #008800; font-style: italic } /* Comment.Hashbang */
.cm { color: #008800; font-style: italic } /* Comment.Multiline */
.cp { color: #008800; font-style: italic } /* Comment.Preproc */
.cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */
.c1 { color: #008800; font-style: italic } /* Comment.Single */
.cs { color: #008800; font-style: italic } /* Comment.Special */
.gd { color: #2c2cff } /* Generic.Deleted */
.ge { color: #008800 } /* Generic.Emph */
.gr { color: #d30202 } /* Generic.Error */
.gh { color: #2c2cff } /* Generic.Heading */
.gi { color: #2c2cff } /* Generic.Inserted */
.go { color: #2c2cff } /* Generic.Output */
.gp { color: #2c2cff } /* Generic.Prompt */
.gs { color: #2c2cff } /* Generic.Strong */
.gu { color: #2c2cff } /* Generic.Subheading */
.gt { color: #2c2cff } /* Generic.Traceback */
.kc { color: #2c2cff; font-weight: bold } /* Keyword.Constant */
.kd { color: #2c2cff } /* Keyword.Declaration */
.kn { color: #2c2cff } /* Keyword.Namespace */
.kp { color: #2c2cff } /* Keyword.Pseudo */
.kr { color: #353580; font-weight: bold } /* Keyword.Reserved */
.kt { color: #2c2cff } /* Keyword.Type */
.m { color: #2c8553; font-weight: bold } /* Literal.Number */
.s { color: #800080 } /* Literal.String */
.nb { color: #2c2cff } /* Name.Builtin */
.nf { font-weight: bold; font-style: italic } /* Name.Function */
.nv { color: #2c2cff; font-weight: bold } /* Name.Variable */
.w { color: #bbbbbb } /* Text.Whitespace */
.mb { color: #2c8553; font-weight: bold } /* Literal.Number.Bin */
.mf { color: #2c8553; font-weight: bold } /* Literal.Number.Float */
.mh { color: #2c8553; font-weight: bold } /* Literal.Number.Hex */
.mi { color: #2c8553; font-weight: bold } /* Literal.Number.Integer */
.mo { color: #2c8553; font-weight: bold } /* Literal.Number.Oct */
.sa { color: #800080 } /* Literal.String.Affix */
.sb { color: #800080 } /* Literal.String.Backtick */
.sc { color: #800080 } /* Literal.String.Char */
.dl { color: #800080 } /* Literal.String.Delimiter */
.sd { color: #800080 } /* Literal.String.Doc */
.s2 { color: #800080 } /* Literal.String.Double */
.se { color: #800080 } /* Literal.String.Escape */
.sh { color: #800080 } /* Literal.String.Heredoc */
.si { color: #800080 } /* Literal.String.Interpol */
.sx { color: #800080 } /* Literal.String.Other */
.sr { color: #800080 } /* Literal.String.Regex */
.s1 { color: #800080 } /* Literal.String.Single */
.ss { color: #800080 } /* Literal.String.Symbol */
.bp { color: #2c2cff } /* Name.Builtin.Pseudo */
.fm { font-weight: bold; font-style: italic } /* Name.Function.Magic */
.vc { color: #2c2cff; font-weight: bold } /* Name.Variable.Class */
.vg { color: #2c2cff; font-weight: bold } /* Name.Variable.Global */
.vi { color: #2c2cff; font-weight: bold } /* Name.Variable.Instance */
.vm { color: #2c2cff; font-weight: bold } /* Name.Variable.Magic */
.il { color: #2c8553; font-weight: bold } /* Literal.Number.Integer.Long */

View File

@ -1,3 +1,9 @@
/* Default theme */
html, body {
margin: 0;
padding: 0;
}
/* Node styling */
.node {
max-width: min(650px, 100ex);
@ -59,21 +65,131 @@ li .tag::after {
/* Code blocks */
pre {
overflow: auto;
padding: 0.5ex;
padding-left: 0.5ex;
padding-left: 1.5ex;
background-color: #eee8d5;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.26);
padding: 0.25ex;
box-shadow: 0px 2px 4px 2px rgba(0, 0, 0, 0.26);
border-radius: 2px;
}
pre > code {
display: block;
line-height: 1.2em;
overflow: auto;
}
code {
pre code {
padding: 0.5ex;
font-size: medium;
border: 2px solid #eee8d5;
background: #fdf6e3;
color: #073642;
background: #fff;
color: #000;
border: none;
font-size: 85%;
}
code {
padding: 0.25ex 0.5ex;
margin: 0.25ex;
background: #eee;
color: #600;
font-family: Menlo, Monaco, "Courier New", monospace;
font-size: 85%;
}
.content {
margin: 1ex;
}
article.post {
max-width: min(650px, 100ex);
margin: 0 auto;
}
/* Header */
.site-header {
background-color: #F7F7FF;
border-bottom: rgba(0,0,0,0.1) 1px solid;
text-align: center;
padding: 1ex;
}
.site-header h1 {
margin-top: 0;
font-size: 200%;
font-family: monospace, sans;
color: #000;
}
.site-header .site-links .fancy-link {
border-right: 1px solid #000;
padding-left: 0.75ex;
}
.site-header .site-links .fancy-link:last-child {
border: none;
}
/* Post header */
.post-metadata ul.post-tags {
list-style: none;
display: inline;
padding: 0;
}
.post-metadata ul.post-tags li.post-tag::before {
content: '#';
}
.post-metadata ul.post-tags li {
display: inline;
}
/* Post index. */
.post-index .post-container {
/* box-shadow: 0px 2px 4px 2px rgba(0, 0, 0, 0.26); */
/* border-radius: 2px; */
/* padding: 1ex; */
margin-bottom: 1em;
padding-bottom: 1em;
border-bottom: #000 1px dashed;
}
/* Dark mode. */
@media (prefers-color-scheme: dark) {
html {
background-color: #111;
color: #fafafe;
}
h2 a {
color: #fafafe;
}
a {
color: #94dcff;
}
h1,h2,h3,h4,h5,h6 {
color: #f7da4a;
}
/* Header */
.site-header {
background-color: #303330;
border-bottom: rgba(0,0,0,0.1) 1px solid;
}
.site-header h1 {
color: #fff;
}
.site-header .site-links .fancy-link {
border-right: 1px solid #fff;
}
/* Code blocks */
pre {
padding: 0.5ex;
background-color: inherit;
box-shadow: none;
}
pre code {
padding: 1ex;
font-size: medium;
border: none;
background: #000;
color: #fff;
}
code {
background: #262826;
color: #FFF;
font-family: Menlo, Monaco, "Courier New", monospace;
}
}