From a8c4d6ef483d34e3a39cd67c0e7df3e3ca6b3714 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Mon, 27 Jun 2022 20:39:21 +0200
Subject: [PATCH 01/21] Add base Markdown blog structure.
---
scripts/blog.py | 157 +++++++++++++++++++++++++++++++++++++++
scripts/requirements.txt | 2 +
static/article.tmpl.html | 15 ++++
static/blog.css | 6 ++
4 files changed, 180 insertions(+)
create mode 100644 scripts/blog.py
create mode 100644 scripts/requirements.txt
create mode 100644 static/article.tmpl.html
create mode 100644 static/blog.css
diff --git a/scripts/blog.py b/scripts/blog.py
new file mode 100644
index 0000000..2e1510c
--- /dev/null
+++ b/scripts/blog.py
@@ -0,0 +1,157 @@
+#!/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 jinja2
+import shutil
+
+import yaml
+import markdown
+import re
+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'
+STATIC_RESOURCES = (
+ ('style.css', 'css/style.css'),
+)
+
+JINJA_ENV = jinja2.Environment(
+ loader=jinja2.FileSystemLoader(STATIC_PATH),
+ autoescape=jinja2.select_autoescape()
+)
+ARTICLE_TEMPLATE = JINJA_ENV.get_template(ARTICLE_TEMPLATE_NAME)
+
+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)))
+
+def parse_complete_date(match):
+ return datetime.datetime.strptime(match.group(0), '%Y-%m-%d %H:%M:%S %Z%z')
+
+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.append((doc, front_matter, out_path))
+ else:
+ raise NotImplementedError('Unknown filetype: {}'.format(name))
+
+ return docs
+
+def render_article(doc, f):
+ result = ARTICLE_TEMPLATE.render(content=doc)
+ f.write(result)
+
+def main(source_top, dest_top):
+ docs = load_all(source_top)
+ for (doc, front_matter, out_path) in docs:
+ 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:
+ render_article(doc, f)
+
+ for src, dest in STATIC_RESOURCES:
+ target_dest = os.path.join(dest_top, dest)
+ os.makedirs(os.path.dirname(target_dest), exist_ok=True)
+ shutil.copy(os.path.join(STATIC_PATH, src), target_dest)
+
+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])
diff --git a/scripts/requirements.txt b/scripts/requirements.txt
new file mode 100644
index 0000000..9878f29
--- /dev/null
+++ b/scripts/requirements.txt
@@ -0,0 +1,2 @@
+Markdown
+Jinja2
diff --git a/static/article.tmpl.html b/static/article.tmpl.html
new file mode 100644
index 0000000..d535fbf
--- /dev/null
+++ b/static/article.tmpl.html
@@ -0,0 +1,15 @@
+
+
+
+
+ C贸digo para llevar
+
+
+
+
+
+
+ {{ content | safe }}
+
+
+
diff --git a/static/blog.css b/static/blog.css
new file mode 100644
index 0000000..eb9a2ef
--- /dev/null
+++ b/static/blog.css
@@ -0,0 +1,6 @@
+body {
+ margin: 0 auto;
+ width: fit-content;
+ max-width: 100ex;
+ padding: 0 1ex;
+}
From 57ed8fa15c6332a5692985902721276e4014c6c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Mon, 27 Jun 2022 21:02:33 +0200
Subject: [PATCH 02/21] Add simple mechanism for fast re-render.
---
scripts/blog.py | 100 ++++++++++++++++++++++++++++++++++-----
static/article.tmpl.html | 2 +-
2 files changed, 89 insertions(+), 13 deletions(-)
diff --git a/scripts/blog.py b/scripts/blog.py
index 2e1510c..fa88c17 100644
--- a/scripts/blog.py
+++ b/scripts/blog.py
@@ -17,12 +17,15 @@ import logging
import sys
import os
import datetime
-import jinja2
import shutil
+import traceback
+import time
+import re
+import jinja2
+import inotify.adapters
import yaml
import markdown
-import re
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})$')
@@ -44,7 +47,24 @@ JINJA_ENV = jinja2.Environment(
loader=jinja2.FileSystemLoader(STATIC_PATH),
autoescape=jinja2.select_autoescape()
)
-ARTICLE_TEMPLATE = JINJA_ENV.get_template(ARTICLE_TEMPLATE_NAME)
+
+def update_statics():
+ global ARTICLE_TEMPLATE
+ ARTICLE_TEMPLATE = JINJA_ENV.get_template(ARTICLE_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)),
@@ -53,9 +73,11 @@ def parse_nikola_date(match):
hour=int(match.group(4)),
minute=int(match.group(5)))
+
def parse_complete_date(match):
return datetime.datetime.strptime(match.group(0), '%Y-%m-%d %H:%M:%S %Z%z')
+
def slugify(title):
"""
Made for compatibility with Nikola's slugify within CodigoParaLlevar blog.
@@ -66,6 +88,7 @@ def slugify(title):
return slug.strip()
+
def read_markdown(path):
with open(path, 'rt') as f:
data = f.read()
@@ -112,7 +135,7 @@ def get_out_path(front_matter):
def load_all(top_dir_relative):
top = os.path.abspath(top_dir_relative)
- docs = []
+ docs = {}
for root, dirs, files in os.walk(top):
for name in files:
@@ -124,29 +147,82 @@ def load_all(top_dir_relative):
path = os.path.join(root, name)
doc, front_matter = read_markdown(path)
out_path = get_out_path(front_matter)
- docs.append((doc, front_matter, out_path))
+ docs[path] = (doc, front_matter, out_path)
else:
raise NotImplementedError('Unknown filetype: {}'.format(name))
return docs
-def render_article(doc, f):
- result = ARTICLE_TEMPLATE.render(content=doc)
+
+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'])
f.write(result)
-def main(source_top, dest_top):
- docs = load_all(source_top)
- for (doc, front_matter, out_path) in docs:
+def regen_all(source_top, dest_top, docs=None):
+ if docs is None:
+ docs = load_all(source_top)
+ 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)
+ # print("==", doc_full_path)
with open(doc_full_path + '.html', 'wt') as f:
- render_article(doc, f)
+ render_article(doc, front_matter, f)
for src, dest in STATIC_RESOURCES:
target_dest = os.path.join(dest_top, dest)
os.makedirs(os.path.dirname(target_dest), exist_ok=True)
shutil.copy(os.path.join(STATIC_PATH, src), target_dest)
+ 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()
+ update_statics()
+ for src, dest in STATIC_RESOURCES:
+ target_dest = os.path.join(dest_top, dest)
+ os.makedirs(os.path.dirname(target_dest), exist_ok=True)
+ shutil.copy(os.path.join(STATIC_PATH, src), target_dest)
+ 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:
+ render_article(doc, front_matter, f)
+ logging.info("Updated all in {:.2f}s".format(time.time() - t0))
+
if __name__ == "__main__":
if len(sys.argv) != 3:
diff --git a/static/article.tmpl.html b/static/article.tmpl.html
index d535fbf..b73f5dd 100644
--- a/static/article.tmpl.html
+++ b/static/article.tmpl.html
@@ -7,8 +7,8 @@
-
+ {{ title }}
{{ content | safe }}
From 33f22f5f0453de0ce596e738b1e42ec624e2d54d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Fri, 15 Jul 2022 00:02:15 +0200
Subject: [PATCH 03/21] Add new collaboration on Hackliza.
---
static/homepage.html | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/static/homepage.html b/static/homepage.html
index c4f1ebf..ea63724 100644
--- a/static/homepage.html
+++ b/static/homepage.html
@@ -80,14 +80,24 @@
From a3ad99cb6122d108fe81d88528ee6973398f2a6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Mon, 18 Jul 2022 22:46:03 +0200
Subject: [PATCH 04/21] Add syntax-highlighting css.
---
scripts/blog.py | 2 +
static/article.tmpl.html | 1 +
static/dark-syntax.css | 83 ++++++++++++++++++++++++++++++++++++++++
static/light-syntax.css | 70 +++++++++++++++++++++++++++++++++
4 files changed, 156 insertions(+)
create mode 100644 static/dark-syntax.css
create mode 100644 static/light-syntax.css
diff --git a/scripts/blog.py b/scripts/blog.py
index fa88c17..354ef5d 100644
--- a/scripts/blog.py
+++ b/scripts/blog.py
@@ -41,6 +41,8 @@ STATIC_PATH = os.path.join(ROOT_DIR, 'static')
ARTICLE_TEMPLATE_NAME = 'article.tmpl.html'
STATIC_RESOURCES = (
('style.css', 'css/style.css'),
+ ('light-syntax.css', 'css/light-syntax.css'),
+ ('dark-syntax.css', 'css/dark-syntax.css'),
)
JINJA_ENV = jinja2.Environment(
diff --git a/static/article.tmpl.html b/static/article.tmpl.html
index b73f5dd..879d557 100644
--- a/static/article.tmpl.html
+++ b/static/article.tmpl.html
@@ -5,6 +5,7 @@
C贸digo para llevar
+
diff --git a/static/dark-syntax.css b/static/dark-syntax.css
new file mode 100644
index 0000000..887e4e5
--- /dev/null
+++ b/static/dark-syntax.css
@@ -0,0 +1,83 @@
+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: #ebdbb2 }
+.c { color: #928374; font-style: italic } /* Comment */
+.err { color: #282828; background-color: #fb4934 } /* Error */
+.esc { color: #dddddd } /* Escape */
+.g { color: #dddddd } /* Generic */
+.k { color: #fb4934 } /* Keyword */
+.l { color: #dddddd } /* Literal */
+.n { color: #dddddd } /* Name */
+.o { color: #dddddd } /* Operator */
+.x { color: #dddddd } /* Other */
+.p { color: #dddddd } /* Punctuation */
+.ch { color: #928374; font-style: italic } /* Comment.Hashbang */
+.cm { color: #928374; font-style: italic } /* Comment.Multiline */
+.c-PreProc { color: #8ec07c; font-style: italic } /* Comment.PreProc */
+.cp { color: #928374; font-style: italic } /* Comment.Preproc */
+.cpf { color: #928374; font-style: italic } /* Comment.PreprocFile */
+.c1 { color: #928374; font-style: italic } /* Comment.Single */
+.cs { color: #ebdbb2; font-weight: bold; font-style: italic } /* Comment.Special */
+.gd { color: #282828; background-color: #fb4934 } /* Generic.Deleted */
+.ge { color: #dddddd; font-style: italic } /* Generic.Emph */
+.gr { color: #fb4934 } /* Generic.Error */
+.gh { color: #ebdbb2; font-weight: bold } /* Generic.Heading */
+.gi { color: #282828; background-color: #b8bb26 } /* Generic.Inserted */
+.go { color: #f2e5bc } /* Generic.Output */
+.gp { color: #a89984 } /* Generic.Prompt */
+.gs { color: #dddddd; font-weight: bold } /* Generic.Strong */
+.gu { color: #ebdbb2; text-decoration: underline } /* Generic.Subheading */
+.gt { color: #fb4934 } /* Generic.Traceback */
+.kc { color: #fb4934 } /* Keyword.Constant */
+.kd { color: #fb4934 } /* Keyword.Declaration */
+.kn { color: #fb4934 } /* Keyword.Namespace */
+.kp { color: #fb4934 } /* Keyword.Pseudo */
+.kr { color: #fb4934 } /* Keyword.Reserved */
+.kt { color: #fb4934 } /* Keyword.Type */
+.ld { color: #dddddd } /* Literal.Date */
+.m { color: #d3869b } /* Literal.Number */
+.s { color: #b8bb26 } /* Literal.String */
+.na { color: #fabd2f } /* Name.Attribute */
+.nb { color: #fe8019 } /* Name.Builtin */
+.nc { color: #8ec07c } /* Name.Class */
+.no { color: #d3869b } /* Name.Constant */
+.nd { color: #fb4934 } /* Name.Decorator */
+.ni { color: #dddddd } /* Name.Entity */
+.ne { color: #fb4934 } /* Name.Exception */
+.nf { color: #8ec07c } /* Name.Function */
+.nl { color: #dddddd } /* Name.Label */
+.nn { color: #8ec07c } /* Name.Namespace */
+.nx { color: #dddddd } /* Name.Other */
+.py { color: #dddddd } /* Name.Property */
+.nt { color: #8ec07c } /* Name.Tag */
+.nv { color: #83a598 } /* Name.Variable */
+.ow { color: #fb4934 } /* Operator.Word */
+.w { color: #dddddd } /* Text.Whitespace */
+.mb { color: #d3869b } /* Literal.Number.Bin */
+.mf { color: #d3869b } /* Literal.Number.Float */
+.mh { color: #d3869b } /* Literal.Number.Hex */
+.mi { color: #d3869b } /* Literal.Number.Integer */
+.mo { color: #d3869b } /* Literal.Number.Oct */
+.sa { color: #b8bb26 } /* Literal.String.Affix */
+.sb { color: #b8bb26 } /* Literal.String.Backtick */
+.sc { color: #b8bb26 } /* Literal.String.Char */
+.dl { color: #b8bb26 } /* Literal.String.Delimiter */
+.sd { color: #b8bb26 } /* Literal.String.Doc */
+.s2 { color: #b8bb26 } /* Literal.String.Double */
+.se { color: #fe8019 } /* Literal.String.Escape */
+.sh { color: #b8bb26 } /* Literal.String.Heredoc */
+.si { color: #b8bb26 } /* Literal.String.Interpol */
+.sx { color: #b8bb26 } /* Literal.String.Other */
+.sr { color: #b8bb26 } /* Literal.String.Regex */
+.s1 { color: #b8bb26 } /* Literal.String.Single */
+.ss { color: #b8bb26 } /* Literal.String.Symbol */
+.bp { color: #fe8019 } /* Name.Builtin.Pseudo */
+.fm { color: #8ec07c } /* Name.Function.Magic */
+.vc { color: #83a598 } /* Name.Variable.Class */
+.vg { color: #83a598 } /* Name.Variable.Global */
+.vi { color: #83a598 } /* Name.Variable.Instance */
+.vm { color: #83a598 } /* Name.Variable.Magic */
+.il { color: #d3869b } /* Literal.Number.Integer.Long */
diff --git a/static/light-syntax.css b/static/light-syntax.css
new file mode 100644
index 0000000..5e91e48
--- /dev/null
+++ b/static/light-syntax.css
@@ -0,0 +1,70 @@
+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: #3c3836 }
+.c { color: #928374; font-style: italic } /* Comment */
+.err { color: #fbf1c7; background-color: #9d0006 } /* Error */
+.k { color: #9d0006 } /* Keyword */
+.ch { color: #928374; font-style: italic } /* Comment.Hashbang */
+.cm { color: #928374; font-style: italic } /* Comment.Multiline */
+.c-PreProc { color: #427b58; font-style: italic } /* Comment.PreProc */
+.cp { color: #928374; font-style: italic } /* Comment.Preproc */
+.cpf { color: #928374; font-style: italic } /* Comment.PreprocFile */
+.c1 { color: #928374; font-style: italic } /* Comment.Single */
+.cs { color: #3c3836; font-weight: bold; font-style: italic } /* Comment.Special */
+.gd { color: #fbf1c7; background-color: #9d0006 } /* Generic.Deleted */
+.ge { font-style: italic } /* Generic.Emph */
+.gr { color: #9d0006 } /* Generic.Error */
+.gh { color: #3c3836; font-weight: bold } /* Generic.Heading */
+.gi { color: #fbf1c7; background-color: #79740e } /* Generic.Inserted */
+.go { color: #32302f } /* Generic.Output */
+.gp { color: #7c6f64 } /* Generic.Prompt */
+.gs { font-weight: bold } /* Generic.Strong */
+.gu { color: #3c3836; text-decoration: underline } /* Generic.Subheading */
+.gt { color: #9d0006 } /* Generic.Traceback */
+.kc { color: #9d0006 } /* Keyword.Constant */
+.kd { color: #9d0006 } /* Keyword.Declaration */
+.kn { color: #9d0006 } /* Keyword.Namespace */
+.kp { color: #9d0006 } /* Keyword.Pseudo */
+.kr { color: #9d0006 } /* Keyword.Reserved */
+.kt { color: #9d0006 } /* Keyword.Type */
+.m { color: #8f3f71 } /* Literal.Number */
+.s { color: #79740e } /* Literal.String */
+.na { color: #b57614 } /* Name.Attribute */
+.nb { color: #af3a03 } /* Name.Builtin */
+.nc { color: #427b58 } /* Name.Class */
+.no { color: #8f3f71 } /* Name.Constant */
+.nd { color: #9d0006 } /* Name.Decorator */
+.ne { color: #9d0006 } /* Name.Exception */
+.nf { color: #427b58 } /* Name.Function */
+.nn { color: #427b58 } /* Name.Namespace */
+.nt { color: #427b58 } /* Name.Tag */
+.nv { color: #076678 } /* Name.Variable */
+.ow { color: #9d0006 } /* Operator.Word */
+.mb { color: #8f3f71 } /* Literal.Number.Bin */
+.mf { color: #8f3f71 } /* Literal.Number.Float */
+.mh { color: #8f3f71 } /* Literal.Number.Hex */
+.mi { color: #8f3f71 } /* Literal.Number.Integer */
+.mo { color: #8f3f71 } /* Literal.Number.Oct */
+.sa { color: #79740e } /* Literal.String.Affix */
+.sb { color: #79740e } /* Literal.String.Backtick */
+.sc { color: #79740e } /* Literal.String.Char */
+.dl { color: #79740e } /* Literal.String.Delimiter */
+.sd { color: #79740e } /* Literal.String.Doc */
+.s2 { color: #79740e } /* Literal.String.Double */
+.se { color: #af3a03 } /* Literal.String.Escape */
+.sh { color: #79740e } /* Literal.String.Heredoc */
+.si { color: #79740e } /* Literal.String.Interpol */
+.sx { color: #79740e } /* Literal.String.Other */
+.sr { color: #79740e } /* Literal.String.Regex */
+.s1 { color: #79740e } /* Literal.String.Single */
+.ss { color: #79740e } /* Literal.String.Symbol */
+.bp { color: #af3a03 } /* Name.Builtin.Pseudo */
+.fm { color: #427b58 } /* Name.Function.Magic */
+.vc { color: #076678 } /* Name.Variable.Class */
+.vg { color: #076678 } /* Name.Variable.Global */
+.vi { color: #076678 } /* Name.Variable.Instance */
+.vm { color: #076678 } /* Name.Variable.Magic */
+.il { color: #8f3f71 } /* Literal.Number.Integer.Long */
From 7afb6be68f29162dbfe6936108e728e3c9963606 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Thu, 21 Jul 2022 21:18:49 +0200
Subject: [PATCH 05/21] Add simplistic autoserve/update mechanism.
---
scripts/autoserve.py | 111 +++++++++++++++++++++++++++++++++++++++++++
wait_for_update.js | 19 ++++++++
2 files changed, 130 insertions(+)
create mode 100644 scripts/autoserve.py
create mode 100644 wait_for_update.js
diff --git a/scripts/autoserve.py b/scripts/autoserve.py
new file mode 100644
index 0000000..a9eb997
--- /dev/null
+++ b/scripts/autoserve.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+
+import sys
+import http.server
+import socketserver
+import threading
+import os
+import time
+import select
+
+import inotify.adapters
+
+PORT = 8000
+
+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('wait_for_update.js', 'rb') as f:
+ new_data = b''
+ 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())
+ for event in notifier.event_gen(yield_nones=False):
+ (ev, types, directory, file) = event
+ if any([type in MONITORED_EVENT_TYPES for type in types]):
+ notify_reloads()
+
+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()
diff --git a/wait_for_update.js b/wait_for_update.js
new file mode 100644
index 0000000..d53b9fd
--- /dev/null
+++ b/wait_for_update.js
@@ -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();
+})();
From 85e254d75c4021a12fa860d69a72c96261b0eb57 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Thu, 21 Jul 2022 23:28:02 +0200
Subject: [PATCH 06/21] Explore more blog-like styling.
---
static/article.tmpl.html | 4 ++--
static/style.css | 21 ++++++++++++++++-----
2 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/static/article.tmpl.html b/static/article.tmpl.html
index 879d557..36d27d1 100644
--- a/static/article.tmpl.html
+++ b/static/article.tmpl.html
@@ -2,13 +2,13 @@
- C贸digo para llevar
+ {{ title }} @ C贸digo para llevar
-
+
{{ title }}
{{ content | safe }}
diff --git a/static/style.css b/static/style.css
index 656ff1b..2ba09d7 100644
--- a/static/style.css
+++ b/static/style.css
@@ -62,7 +62,7 @@ pre {
padding: 0.5ex;
padding-left: 0.5ex;
padding-left: 1.5ex;
- background-color: #eee8d5;
+ background-color: #001622;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.26);
}
pre > code {
@@ -70,10 +70,21 @@ pre > code {
line-height: 1.2em;
overflow: auto;
}
-code {
+pre code {
padding: 0.5ex;
font-size: medium;
- border: 2px solid #eee8d5;
- background: #fdf6e3;
- color: #073642;
+ border: 2px solid #001622;
+ background: #073642;
+ color: #fff;
+}
+
+code {
+ padding: 0.25ex;
+ margin: 0.25ex;
+ border: 1px solid #001622;
+}
+
+article.post {
+ max-width: min(650px, 100ex);
+ margin: 0 auto;
}
From 612c38711d25b2e42a52cb6f74a35c89e12423f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Thu, 21 Jul 2022 23:30:47 +0200
Subject: [PATCH 07/21] Add basic dark-mode style.
---
static/style.css | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/static/style.css b/static/style.css
index 2ba09d7..a1ab463 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,3 +1,4 @@
+/* Default theme */
/* Node styling */
.node {
max-width: min(650px, 100ex);
@@ -88,3 +89,21 @@ article.post {
max-width: min(650px, 100ex);
margin: 0 auto;
}
+
+/* Dark mode. */
+@media (prefers-color-scheme: dark) {
+ html {
+ background-color: #0f110e;
+ color: #fafafe;
+ }
+ h2 a {
+ color: #fafafe;
+ }
+ a {
+ color: #aaf;
+ }
+ h1,h2,h3,h4,h5,h6 {
+ margin-top: 0;
+ color: #adbac7;
+ }
+}
From ef6072fd279275e96dd4034c564408ed3d28c46b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Fri, 22 Jul 2022 00:23:07 +0200
Subject: [PATCH 08/21] [minor] Fast styling touches to posts's dark mode.
---
static/style.css | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/static/style.css b/static/style.css
index a1ab463..5dae5df 100644
--- a/static/style.css
+++ b/static/style.css
@@ -93,17 +93,17 @@ article.post {
/* Dark mode. */
@media (prefers-color-scheme: dark) {
html {
- background-color: #0f110e;
+ background-color: #111;
color: #fafafe;
}
h2 a {
color: #fafafe;
}
a {
- color: #aaf;
+ color: #99f;
}
h1,h2,h3,h4,h5,h6 {
margin-top: 0;
- color: #adbac7;
+ color: #f7da4a;
}
}
From 4ebbd47a61cbcd8b80263525ff2f9e08ff742d4a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Fri, 22 Jul 2022 17:58:28 +0200
Subject: [PATCH 09/21] Improve link contrast against background.
---
static/style.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/static/style.css b/static/style.css
index 5dae5df..cdb1f70 100644
--- a/static/style.css
+++ b/static/style.css
@@ -100,7 +100,7 @@ article.post {
color: #fafafe;
}
a {
- color: #99f;
+ color: #94dcff;
}
h1,h2,h3,h4,h5,h6 {
margin-top: 0;
From 31a303ab55827ada797d27972e1e194e2f57e08a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Fri, 22 Jul 2022 18:16:48 +0200
Subject: [PATCH 10/21] Cleaner code blocks in dark mode.
---
static/style.css | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/static/style.css b/static/style.css
index cdb1f70..34bac6a 100644
--- a/static/style.css
+++ b/static/style.css
@@ -106,4 +106,23 @@ article.post {
margin-top: 0;
color: #f7da4a;
}
+ /* Code blocks */
+ pre {
+ padding: 0.5ex;
+ background-color: inherit;
+ box-shadow: none;
+ }
+ pre code {
+ padding: 1ex;
+ font-size: medium;
+ background: #000;
+ color: #fff;
+ border: none;
+ }
+
+ code {
+ padding: 0.25ex;
+ margin: 0.25ex;
+ border: 1px solid #00000;
+ }
}
From 29a9d253812810448f5fb213115dccf0aab657d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Sun, 24 Jul 2022 23:08:38 +0200
Subject: [PATCH 11/21] Add auto-wrapping media queries for CSS static
resources.
---
scripts/blog.py | 38 +++++++++++++++++++++++++++++++-------
1 file changed, 31 insertions(+), 7 deletions(-)
diff --git a/scripts/blog.py b/scripts/blog.py
index 354ef5d..050f0ec 100644
--- a/scripts/blog.py
+++ b/scripts/blog.py
@@ -42,7 +42,7 @@ ARTICLE_TEMPLATE_NAME = 'article.tmpl.html'
STATIC_RESOURCES = (
('style.css', 'css/style.css'),
('light-syntax.css', 'css/light-syntax.css'),
- ('dark-syntax.css', 'css/dark-syntax.css'),
+ ('dark-syntax.css', 'css/dark-syntax.css', ('@media (prefers-color-scheme: dark) {\n', '\n}')),
)
JINJA_ENV = jinja2.Environment(
@@ -176,10 +176,22 @@ def regen_all(source_top, dest_top, docs=None):
with open(doc_full_path + '.html', 'wt') as f:
render_article(doc, front_matter, f)
- for src, dest in STATIC_RESOURCES:
- target_dest = os.path.join(dest_top, dest)
+ 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)
- shutil.copy(os.path.join(STATIC_PATH, src), target_dest)
+ 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)
+
return docs
@@ -201,10 +213,22 @@ def main(source_top, dest_top):
if filepath.startswith(STATIC_PATH):
t0 = time.time()
update_statics()
- for src, dest in STATIC_RESOURCES:
- target_dest = os.path.join(dest_top, dest)
+ 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)
- shutil.copy(os.path.join(STATIC_PATH, src), target_dest)
+ 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)
+
docs = regen_all(source_top, dest_top, docs)
logging.info("Updated all in {:.2f}s".format(time.time() - t0))
From 830c26e3333cd43c77d89c0ece366db10892d9e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Sun, 24 Jul 2022 23:09:05 +0200
Subject: [PATCH 12/21] Add light syntax, using Pygments `sas` style.
---
static/article.tmpl.html | 1 +
static/light-syntax.css | 124 +++++++++++++++++++--------------------
static/style.css | 14 ++---
3 files changed, 65 insertions(+), 74 deletions(-)
diff --git a/static/article.tmpl.html b/static/article.tmpl.html
index 36d27d1..7f11585 100644
--- a/static/article.tmpl.html
+++ b/static/article.tmpl.html
@@ -5,6 +5,7 @@
{{ title }} @ C贸digo para llevar
+
diff --git a/static/light-syntax.css b/static/light-syntax.css
index 5e91e48..e4813ac 100644
--- a/static/light-syntax.css
+++ b/static/light-syntax.css
@@ -3,68 +3,62 @@ td.linenos .normal { color: inherit; background-color: transparent; padding-left
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: #3c3836 }
-.c { color: #928374; font-style: italic } /* Comment */
-.err { color: #fbf1c7; background-color: #9d0006 } /* Error */
-.k { color: #9d0006 } /* Keyword */
-.ch { color: #928374; font-style: italic } /* Comment.Hashbang */
-.cm { color: #928374; font-style: italic } /* Comment.Multiline */
-.c-PreProc { color: #427b58; font-style: italic } /* Comment.PreProc */
-.cp { color: #928374; font-style: italic } /* Comment.Preproc */
-.cpf { color: #928374; font-style: italic } /* Comment.PreprocFile */
-.c1 { color: #928374; font-style: italic } /* Comment.Single */
-.cs { color: #3c3836; font-weight: bold; font-style: italic } /* Comment.Special */
-.gd { color: #fbf1c7; background-color: #9d0006 } /* Generic.Deleted */
-.ge { font-style: italic } /* Generic.Emph */
-.gr { color: #9d0006 } /* Generic.Error */
-.gh { color: #3c3836; font-weight: bold } /* Generic.Heading */
-.gi { color: #fbf1c7; background-color: #79740e } /* Generic.Inserted */
-.go { color: #32302f } /* Generic.Output */
-.gp { color: #7c6f64 } /* Generic.Prompt */
-.gs { font-weight: bold } /* Generic.Strong */
-.gu { color: #3c3836; text-decoration: underline } /* Generic.Subheading */
-.gt { color: #9d0006 } /* Generic.Traceback */
-.kc { color: #9d0006 } /* Keyword.Constant */
-.kd { color: #9d0006 } /* Keyword.Declaration */
-.kn { color: #9d0006 } /* Keyword.Namespace */
-.kp { color: #9d0006 } /* Keyword.Pseudo */
-.kr { color: #9d0006 } /* Keyword.Reserved */
-.kt { color: #9d0006 } /* Keyword.Type */
-.m { color: #8f3f71 } /* Literal.Number */
-.s { color: #79740e } /* Literal.String */
-.na { color: #b57614 } /* Name.Attribute */
-.nb { color: #af3a03 } /* Name.Builtin */
-.nc { color: #427b58 } /* Name.Class */
-.no { color: #8f3f71 } /* Name.Constant */
-.nd { color: #9d0006 } /* Name.Decorator */
-.ne { color: #9d0006 } /* Name.Exception */
-.nf { color: #427b58 } /* Name.Function */
-.nn { color: #427b58 } /* Name.Namespace */
-.nt { color: #427b58 } /* Name.Tag */
-.nv { color: #076678 } /* Name.Variable */
-.ow { color: #9d0006 } /* Operator.Word */
-.mb { color: #8f3f71 } /* Literal.Number.Bin */
-.mf { color: #8f3f71 } /* Literal.Number.Float */
-.mh { color: #8f3f71 } /* Literal.Number.Hex */
-.mi { color: #8f3f71 } /* Literal.Number.Integer */
-.mo { color: #8f3f71 } /* Literal.Number.Oct */
-.sa { color: #79740e } /* Literal.String.Affix */
-.sb { color: #79740e } /* Literal.String.Backtick */
-.sc { color: #79740e } /* Literal.String.Char */
-.dl { color: #79740e } /* Literal.String.Delimiter */
-.sd { color: #79740e } /* Literal.String.Doc */
-.s2 { color: #79740e } /* Literal.String.Double */
-.se { color: #af3a03 } /* Literal.String.Escape */
-.sh { color: #79740e } /* Literal.String.Heredoc */
-.si { color: #79740e } /* Literal.String.Interpol */
-.sx { color: #79740e } /* Literal.String.Other */
-.sr { color: #79740e } /* Literal.String.Regex */
-.s1 { color: #79740e } /* Literal.String.Single */
-.ss { color: #79740e } /* Literal.String.Symbol */
-.bp { color: #af3a03 } /* Name.Builtin.Pseudo */
-.fm { color: #427b58 } /* Name.Function.Magic */
-.vc { color: #076678 } /* Name.Variable.Class */
-.vg { color: #076678 } /* Name.Variable.Global */
-.vi { color: #076678 } /* Name.Variable.Instance */
-.vm { color: #076678 } /* Name.Variable.Magic */
-.il { color: #8f3f71 } /* Literal.Number.Integer.Long */
+.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 */
diff --git a/static/style.css b/static/style.css
index 34bac6a..025d35c 100644
--- a/static/style.css
+++ b/static/style.css
@@ -60,11 +60,9 @@ li .tag::after {
/* Code blocks */
pre {
overflow: auto;
- padding: 0.5ex;
- padding-left: 0.5ex;
- padding-left: 1.5ex;
- background-color: #001622;
- 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;
@@ -74,15 +72,13 @@ pre > code {
pre code {
padding: 0.5ex;
font-size: medium;
- border: 2px solid #001622;
- background: #073642;
- color: #fff;
+ background: #fff;
+ color: #000;
}
code {
padding: 0.25ex;
margin: 0.25ex;
- border: 1px solid #001622;
}
article.post {
From ba9eba887baf2fd2d4e30a2ea4d08a005e8dfccd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Sun, 24 Jul 2022 23:09:27 +0200
Subject: [PATCH 13/21] Move dark-mode to monokai.
---
static/dark-syntax.css | 155 ++++++++++++++++++++---------------------
1 file changed, 77 insertions(+), 78 deletions(-)
diff --git a/static/dark-syntax.css b/static/dark-syntax.css
index 887e4e5..2938240 100644
--- a/static/dark-syntax.css
+++ b/static/dark-syntax.css
@@ -3,81 +3,80 @@ td.linenos .normal { color: inherit; background-color: transparent; padding-left
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: #ebdbb2 }
-.c { color: #928374; font-style: italic } /* Comment */
-.err { color: #282828; background-color: #fb4934 } /* Error */
-.esc { color: #dddddd } /* Escape */
-.g { color: #dddddd } /* Generic */
-.k { color: #fb4934 } /* Keyword */
-.l { color: #dddddd } /* Literal */
-.n { color: #dddddd } /* Name */
-.o { color: #dddddd } /* Operator */
-.x { color: #dddddd } /* Other */
-.p { color: #dddddd } /* Punctuation */
-.ch { color: #928374; font-style: italic } /* Comment.Hashbang */
-.cm { color: #928374; font-style: italic } /* Comment.Multiline */
-.c-PreProc { color: #8ec07c; font-style: italic } /* Comment.PreProc */
-.cp { color: #928374; font-style: italic } /* Comment.Preproc */
-.cpf { color: #928374; font-style: italic } /* Comment.PreprocFile */
-.c1 { color: #928374; font-style: italic } /* Comment.Single */
-.cs { color: #ebdbb2; font-weight: bold; font-style: italic } /* Comment.Special */
-.gd { color: #282828; background-color: #fb4934 } /* Generic.Deleted */
-.ge { color: #dddddd; font-style: italic } /* Generic.Emph */
-.gr { color: #fb4934 } /* Generic.Error */
-.gh { color: #ebdbb2; font-weight: bold } /* Generic.Heading */
-.gi { color: #282828; background-color: #b8bb26 } /* Generic.Inserted */
-.go { color: #f2e5bc } /* Generic.Output */
-.gp { color: #a89984 } /* Generic.Prompt */
-.gs { color: #dddddd; font-weight: bold } /* Generic.Strong */
-.gu { color: #ebdbb2; text-decoration: underline } /* Generic.Subheading */
-.gt { color: #fb4934 } /* Generic.Traceback */
-.kc { color: #fb4934 } /* Keyword.Constant */
-.kd { color: #fb4934 } /* Keyword.Declaration */
-.kn { color: #fb4934 } /* Keyword.Namespace */
-.kp { color: #fb4934 } /* Keyword.Pseudo */
-.kr { color: #fb4934 } /* Keyword.Reserved */
-.kt { color: #fb4934 } /* Keyword.Type */
-.ld { color: #dddddd } /* Literal.Date */
-.m { color: #d3869b } /* Literal.Number */
-.s { color: #b8bb26 } /* Literal.String */
-.na { color: #fabd2f } /* Name.Attribute */
-.nb { color: #fe8019 } /* Name.Builtin */
-.nc { color: #8ec07c } /* Name.Class */
-.no { color: #d3869b } /* Name.Constant */
-.nd { color: #fb4934 } /* Name.Decorator */
-.ni { color: #dddddd } /* Name.Entity */
-.ne { color: #fb4934 } /* Name.Exception */
-.nf { color: #8ec07c } /* Name.Function */
-.nl { color: #dddddd } /* Name.Label */
-.nn { color: #8ec07c } /* Name.Namespace */
-.nx { color: #dddddd } /* Name.Other */
-.py { color: #dddddd } /* Name.Property */
-.nt { color: #8ec07c } /* Name.Tag */
-.nv { color: #83a598 } /* Name.Variable */
-.ow { color: #fb4934 } /* Operator.Word */
-.w { color: #dddddd } /* Text.Whitespace */
-.mb { color: #d3869b } /* Literal.Number.Bin */
-.mf { color: #d3869b } /* Literal.Number.Float */
-.mh { color: #d3869b } /* Literal.Number.Hex */
-.mi { color: #d3869b } /* Literal.Number.Integer */
-.mo { color: #d3869b } /* Literal.Number.Oct */
-.sa { color: #b8bb26 } /* Literal.String.Affix */
-.sb { color: #b8bb26 } /* Literal.String.Backtick */
-.sc { color: #b8bb26 } /* Literal.String.Char */
-.dl { color: #b8bb26 } /* Literal.String.Delimiter */
-.sd { color: #b8bb26 } /* Literal.String.Doc */
-.s2 { color: #b8bb26 } /* Literal.String.Double */
-.se { color: #fe8019 } /* Literal.String.Escape */
-.sh { color: #b8bb26 } /* Literal.String.Heredoc */
-.si { color: #b8bb26 } /* Literal.String.Interpol */
-.sx { color: #b8bb26 } /* Literal.String.Other */
-.sr { color: #b8bb26 } /* Literal.String.Regex */
-.s1 { color: #b8bb26 } /* Literal.String.Single */
-.ss { color: #b8bb26 } /* Literal.String.Symbol */
-.bp { color: #fe8019 } /* Name.Builtin.Pseudo */
-.fm { color: #8ec07c } /* Name.Function.Magic */
-.vc { color: #83a598 } /* Name.Variable.Class */
-.vg { color: #83a598 } /* Name.Variable.Global */
-.vi { color: #83a598 } /* Name.Variable.Instance */
-.vm { color: #83a598 } /* Name.Variable.Magic */
-.il { color: #d3869b } /* Literal.Number.Integer.Long */
+.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 */
From 031305f5deaa04436e29a3ab8eb227dafdf23c0c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Sun, 24 Jul 2022 23:56:29 +0200
Subject: [PATCH 14/21] Add style for inline code.
---
static/style.css | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/static/style.css b/static/style.css
index 025d35c..191a919 100644
--- a/static/style.css
+++ b/static/style.css
@@ -74,11 +74,17 @@ pre code {
font-size: medium;
background: #fff;
color: #000;
+ border: none;
+ font-size: 85%;
}
code {
- padding: 0.25ex;
+ padding: 0.25ex 0.5ex;
margin: 0.25ex;
+ background: #eee;
+ color: #600;
+ font-family: Menlo, Monaco, "Courier New", monospace;
+ font-size: 85%;
}
article.post {
@@ -111,14 +117,16 @@ article.post {
pre code {
padding: 1ex;
font-size: medium;
+ border: none;
background: #000;
color: #fff;
- border: none;
}
code {
- padding: 0.25ex;
- margin: 0.25ex;
- border: 1px solid #00000;
+ background: #262826;
+ color: #FFF;
+ font-family: Menlo, Monaco, "Courier New", monospace;
+ font-size: 85%;
+
}
}
From 8d831af216f09fac9cec72d0a10c91991e0fd0ab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Mon, 25 Jul 2022 12:19:10 +0200
Subject: [PATCH 15/21] Add blog/post header.
---
static/article.tmpl.html | 27 ++++++++++++++++++++++----
static/style.css | 41 +++++++++++++++++++++++++++++++++++++++-
2 files changed, 63 insertions(+), 5 deletions(-)
diff --git a/static/article.tmpl.html b/static/article.tmpl.html
index 7f11585..144132b 100644
--- a/static/article.tmpl.html
+++ b/static/article.tmpl.html
@@ -9,9 +9,28 @@
-
- {{ title }}
- {{ content | safe }}
-
+
+
+
+ {{ title }}
+ {{ content | safe }}
+
+
diff --git a/static/style.css b/static/style.css
index 191a919..47d189e 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,4 +1,9 @@
/* Default theme */
+html, body {
+ margin: 0;
+ padding: 0;
+}
+
/* Node styling */
.node {
max-width: min(650px, 100ex);
@@ -87,10 +92,33 @@ code {
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%;
+}
+.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;
+}
+
/* Dark mode. */
@media (prefers-color-scheme: dark) {
@@ -108,6 +136,18 @@ article.post {
margin-top: 0;
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;
@@ -127,6 +167,5 @@ article.post {
color: #FFF;
font-family: Menlo, Monaco, "Courier New", monospace;
font-size: 85%;
-
}
}
From feefd1f4d57deca286a74a223644ea00203a8b6a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Mon, 25 Jul 2022 12:19:32 +0200
Subject: [PATCH 16/21] Improve autoserve logic, reduce repeated reloads.
---
scripts/autoserve.py | 17 ++++++++++++++---
scripts/blog.py | 10 ++++++++--
.../wait_for_update.js | 0
3 files changed, 22 insertions(+), 5 deletions(-)
rename wait_for_update.js => scripts/wait_for_update.js (100%)
diff --git a/scripts/autoserve.py b/scripts/autoserve.py
index a9eb997..6e69025 100644
--- a/scripts/autoserve.py
+++ b/scripts/autoserve.py
@@ -12,6 +12,8 @@ import inotify.adapters
PORT = 8000
+THIS_DIR = os.path.dirname(os.path.abspath(__file__))
+
MONITORED_EVENT_TYPES = (
'IN_CREATE',
# 'IN_MODIFY',
@@ -66,7 +68,7 @@ class Server(http.server.SimpleHTTPRequestHandler):
return
else:
# Append update waiter
- with open('wait_for_update.js', 'rb') as f:
+ with open(os.path.join(THIS_DIR, 'wait_for_update.js'), 'rb') as f:
new_data = b''
self.wfile.write(new_data)
new_data_len = len(new_data)
@@ -92,10 +94,19 @@ def notify_reloads():
def start_notifier():
notifier = inotify.adapters.InotifyTree(os.getcwd())
- for event in notifier.event_gen(yield_nones=False):
+ 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]):
- notify_reloads()
+ print("Detected change!", types, directory, file)
+ should_reload = True
def serve():
Handler = Server
diff --git a/scripts/blog.py b/scripts/blog.py
index 050f0ec..82f1cf3 100644
--- a/scripts/blog.py
+++ b/scripts/blog.py
@@ -213,9 +213,12 @@ def main(source_top, dest_top):
if filepath.startswith(STATIC_PATH):
t0 = time.time()
update_statics()
+ 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]
@@ -229,8 +232,11 @@ def main(source_top, dest_top):
with open(target_dest, 'wt') as f:
f.write(data)
- docs = regen_all(source_top, dest_top, docs)
- logging.info("Updated all in {:.2f}s".format(time.time() - t0))
+ 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:
diff --git a/wait_for_update.js b/scripts/wait_for_update.js
similarity index 100%
rename from wait_for_update.js
rename to scripts/wait_for_update.js
From 189a94c9307c957c6cc9495184123a486a091cc7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Mon, 25 Jul 2022 17:58:58 +0200
Subject: [PATCH 17/21] Add simple date, tags to posts.
---
scripts/blog.py | 39 +++++++++++++++++++++++++++++++++++----
static/article.tmpl.html | 14 +++++++++++++-
static/style.css | 18 ++++++++++++++++--
3 files changed, 64 insertions(+), 7 deletions(-)
diff --git a/scripts/blog.py b/scripts/blog.py
index 82f1cf3..93d7c8f 100644
--- a/scripts/blog.py
+++ b/scripts/blog.py
@@ -21,6 +21,7 @@ import shutil
import traceback
import time
import re
+from typing import List
import jinja2
import inotify.adapters
@@ -79,6 +80,13 @@ def parse_nikola_date(match):
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):
"""
@@ -163,7 +171,12 @@ def load_doc(filepath):
def render_article(doc, front_matter, f):
- result = ARTICLE_TEMPLATE.render(content=doc, title=front_matter['title'])
+ 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 regen_all(source_top, dest_top, docs=None):
@@ -174,7 +187,13 @@ def regen_all(source_top, dest_top, docs=None):
os.makedirs(os.path.dirname(doc_full_path), exist_ok=True)
# print("==", doc_full_path)
with open(doc_full_path + '.html', 'wt') as f:
- render_article(doc, front_matter, f)
+ try:
+ render_article(doc, front_matter, f)
+ except:
+ logging.error(traceback.format_exc())
+ logging.error("Rendering failed 馃樋")
+ continue
+
for static in STATIC_RESOURCES:
src_path = static[0]
@@ -212,7 +231,13 @@ def main(source_top, dest_top):
filepath = os.path.join(directory, file)
if filepath.startswith(STATIC_PATH):
t0 = time.time()
- update_statics()
+ 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]
@@ -252,7 +277,13 @@ def main(source_top, dest_top):
os.makedirs(os.path.dirname(doc_full_path), exist_ok=True)
# print("==", doc_full_path)
with open(doc_full_path + '.html', 'wt') as f:
- render_article(doc, front_matter, 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))
diff --git a/static/article.tmpl.html b/static/article.tmpl.html
index 144132b..d2750ea 100644
--- a/static/article.tmpl.html
+++ b/static/article.tmpl.html
@@ -29,7 +29,19 @@
{{ title }}
- {{ content | safe }}
+
+
+ {{ post_publication_date }}
+
+
+ {% for post_tag in post_tags %}
+ {{ post_tag }}
+ {% endfor %}
+
+
+
+ {{ content | safe }}
+
diff --git a/static/style.css b/static/style.css
index 47d189e..d00336f 100644
--- a/static/style.css
+++ b/static/style.css
@@ -110,6 +110,8 @@ article.post {
.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;
@@ -119,6 +121,20 @@ article.post {
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;
+}
+
/* Dark mode. */
@media (prefers-color-scheme: dark) {
@@ -133,7 +149,6 @@ article.post {
color: #94dcff;
}
h1,h2,h3,h4,h5,h6 {
- margin-top: 0;
color: #f7da4a;
}
/* Header */
@@ -166,6 +181,5 @@ article.post {
background: #262826;
color: #FFF;
font-family: Menlo, Monaco, "Courier New", monospace;
- font-size: 85%;
}
}
From 9483fc1ba48a76b310b520cd1c9996217dca460c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?=
Date: Wed, 27 Jul 2022 23:52:01 +0200
Subject: [PATCH 18/21] Add minor style changes on homepage.
---
static/homepage.html | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/static/homepage.html b/static/homepage.html
index ea63724..01bb87e 100644
--- a/static/homepage.html
+++ b/static/homepage.html
@@ -5,12 +5,16 @@
C贸digo para llevar
{{ title }}
{{ content | safe }}{{ title }}
- {{ content | safe }} -Codigo para llevar
+ +{{ title }}
+ {{ content | safe }} +{{ title }}
- {{ content | safe }} ++ {% for post_tag in post_tags %} +- {{ post_tag }}
+ {% endfor %}
+
+