Compare commits

..

2 Commits

18 changed files with 248 additions and 56 deletions

1
.gitattributes vendored
View File

@ -3,3 +3,4 @@
# Ignore Jupyter Notebooks in language stats. # Ignore Jupyter Notebooks in language stats.
*.ipynb linguist-vendored *.ipynb linguist-vendored
site/assets/css/codehilite.css linguist-vendored

View File

@ -8,19 +8,12 @@ sites:
build_cache: ./site build_cache: ./site
assets: assets:
- /assets - /assets
- robots.txt
articles: articles:
- ./pages/*.md - '*.md'
template_selections: template_selections:
article: templates.components.simple_article article: templates.components.simple_article
robots_txt:
title: robots.txt
base_url: http://localhost:8000/robots.txt
web_root: ./dist
build_cache: ./site
assets:
- robots.txt
resume: resume:
title: Resume title: Resume
base_url: http://localhost:8000 base_url: http://localhost:8000
@ -50,6 +43,20 @@ sites:
addons: addons:
- rss - rss
# TODO: add Jimoire Laboratoire jimoire_laboratoire:
title: Jimoire Laboratoire
description: How I built the Jimlab, and you can too!
base_url: http://localhost:8000/jimoire-laboratoire
git_repo:
url: ssh://gitea/jimlab/jimoire-laboratoire.git
branch: jimsite
lfs: false
build_cache: ./build/jimoire-laboratoire
web_root: ./dist/jimoire-laboratoire
assets: []
articles:
- '*.md'
- 'essentials/*.md'
- '**/*.md'
# TODO: add newsletter # TODO: add newsletter

View File

@ -5,6 +5,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# TODO: add a way to change log level
from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access
from .templating import format_html_template, map_templates, TemplateSelections from .templating import format_html_template, map_templates, TemplateSelections

View File

@ -2,7 +2,10 @@ import os
import glob import glob
import yaml import yaml
import markdown import markdown
from pygments.formatters import HtmlFormatter
from markdown.extensions.codehilite import CodeHiliteExtension
import pydantic import pydantic
import logging
from typing import Optional, Union from typing import Optional, Union
from dotmap import DotMap from dotmap import DotMap
from datetime import date from datetime import date
@ -10,12 +13,37 @@ from datetime import date
from .common import filepath_or_string, SiteConfig from .common import filepath_or_string, SiteConfig
from .templating import format_html_template, TemplateSelections from .templating import format_html_template, TemplateSelections
logger = logging.getLogger(__name__)
class CustomHiliteFormatter(HtmlFormatter):
'''
Override the default Pygments formatter (used by markdown.extensions.codehilite
to add syntax highlighting to fenced code) to include the `language-*` class.
From https://python-markdown.github.io/extensions/code_hilite/
'''
def __init__(self, lang_str='', **options):
super().__init__(**options)
self.lang_str = lang_str
def _wrap_code(self, source):
yield 0, f'<code class="{self.lang_str}">'
yield from source
yield 0, '</code>'
# Pick extensions to use for Markdown parsing.
MARKDOWN_EXTENSIONS = [
'footnotes',
'fenced_code',
CodeHiliteExtension(linenums = True, pygments_formatter = CustomHiliteFormatter)
]
class ArticleMetadata(pydantic.BaseModel): class ArticleMetadata(pydantic.BaseModel):
'''A model for the YAML frontmatter included with Markdown articles.''' '''A model for the YAML frontmatter included with Markdown articles.'''
title: str title: str
date: date date: date
published: bool tags: Optional[list] = []
tags: list published: Optional[bool] = False
author: Optional[str] = None author: Optional[str] = None
lastmod: Optional[date] = None lastmod: Optional[date] = None
thumbnail: Optional[str] = None thumbnail: Optional[str] = None
@ -42,11 +70,13 @@ def load_markdown(md: str) -> tuple[Optional[ArticleMetadata], str]:
# Split the metadata from the contents. # Split the metadata from the contents.
[raw_metadata, raw_article] = md.split('---') [raw_metadata, raw_article] = md.split('---')
# TODO: Consider adding an optional postscript document, for things like unescaped CSS.
# Use YAML to parse the metadata. # Use YAML to parse the metadata.
metadata = yaml.safe_load(raw_metadata) metadata = yaml.safe_load(raw_metadata)
# Convert the contents to a HTML string. # Convert the contents to a HTML string.
content = markdown.markdown(raw_article) content = markdown.markdown(raw_article, extensions = MARKDOWN_EXTENSIONS)
return ArticleMetadata(**metadata), content return ArticleMetadata(**metadata), content
@ -66,21 +96,23 @@ def build_index(site: SiteConfig) -> dict[str, Article]:
) )
for article_full_path in expanded_article_list: for article_full_path in set(expanded_article_list):
logger.debug(f'Loading article from {article_full_path}')
metadata, content = load_markdown(article_full_path) metadata, content = load_markdown(article_full_path)
# TODO: consider adding support for articles with no metadata
if metadata is None:
logger.debug(f'Article loaded from {article_full_path} has no metadata. Skipping.')
continue
# Skip unpublished articles. # Skip unpublished articles.
if not metadata.published: if not metadata.published:
logger.debug(f'Article loaded from {article_full_path} is not published. Skipping.')
continue continue
# Construct the article's path for the index by discarding the build cache # Construct the article's path for the index by discarding the build cache.
# and replacing .md with .html. article_path = article_full_path.removeprefix(site.build_cache).lstrip("/").replace('.md','.html')
# article_path = article_full_path\
# .removeprefix(site.build_cache)\
# .lstrip('/')\
# .replace('.md','.html')
# TODO: add tolerance for a hierarchical article directory structure.
article_path = os.path.basename(article_full_path).replace('.md', '.html')
index[article_path] = Article( index[article_path] = Article(
path = article_path, path = article_path,
@ -147,7 +179,14 @@ def build_articles(
) )
with open(f"{site.web_root.rstrip('/')}/{article.path}", 'w') as f: # Construct the path to which the article page is to be saved.
destination = f"{site.web_root.rstrip('/')}/{article.path}"
# Ensure the directory structure exists.
os.makedirs(os.path.dirname(destination), exist_ok=True)
# Save the file.
with open(destination, 'w') as f:
f.write(page_html) f.write(page_html)

View File

@ -1,8 +1,10 @@
import os import os
import glob import glob
import shutil import shutil
import logging
from .common import run, GitRepo, SiteConfig from .common import run, GitRepo, SiteConfig
logger = logging.getLogger(__name__)
def pull_git_repo(repo: GitRepo, build_cache: str) -> None: def pull_git_repo(repo: GitRepo, build_cache: str) -> None:
'''Pulls/clones a repo into the build cache directory.''' '''Pulls/clones a repo into the build cache directory.'''
@ -49,7 +51,7 @@ def copy_assets(site: SiteConfig) -> None:
glob.glob(f'{site.build_cache}/{a.lstrip("/")}') glob.glob(f'{site.build_cache}/{a.lstrip("/")}')
) )
for asset in expanded_asset_list: for asset in set(expanded_asset_list):
# Skip ignored assets. # Skip ignored assets.
if asset in ignored_asset_list: if asset in ignored_asset_list:

View File

@ -58,9 +58,9 @@ def build_blog_archive(
tag_selector_options = [] tag_selector_options = []
tag_selector_css_rules = [f''' tag_selector_css_rules = [f'''
body:has(input[name="tag-selector"][value="*"]:checked) li:has(.blog-tag){{{{ body:has(input[name="tag-selector"][value="*"]:checked) li:has(.blog-tag){{
display: list-item!important; display: list-item!important;
}}}} }}
'''] ''']
# Add tag selector options in descending order of article count. # Add tag selector options in descending order of article count.
@ -75,12 +75,12 @@ def build_blog_archive(
if tag == '*': if tag == '*':
continue continue
tag_selector_css_rules.append(f''' tag_selector_css_rules.append(f'''
body:has(input[name="tag-selector"]:not([value="{tag}"]):checked) li:has(.blog-tag[data="{tag}"]){{{{ body:has(input[name="tag-selector"]:not([value="{tag}"]):checked) li:has(.blog-tag[data="{tag}"]){{
display: none; display: none;
}}}} }}
body:has(input[name="tag-selector"][value="{tag}"]:checked) li:has(.blog-tag[data="{tag}"]){{{{ body:has(input[name="tag-selector"][value="{tag}"]:checked) li:has(.blog-tag[data="{tag}"]){{
display: list-item!important; display: list-item!important;
}}}} }}
''') ''')
# For Python 3.9-friendliness. # For Python 3.9-friendliness.

View File

@ -1,18 +1,52 @@
import os import os
import re
# Use the community regex package instead of re for QoL
# features like variable-length lookbacks.
import regex as re
import logging
from dotmap import DotMap from dotmap import DotMap
logger = logging.getLogger(__name__)
from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access
def escape_braces(template) -> str:
'''Escapes any curly braces that are unclosed or enclose
anything other than a pattern for a valid variable
(alphanumerics, underscores, and periods ONLY;
must start with alpha or underscore).
'''
# Match any left braces that are not followed by a valid variable
# name (with no whitespace) and a closing right brace.
left_brace_pattern = r'{(?![A-Za-z_][\w.]+})'
# Match any right braces that are not preceded by an opening
# left brace and a valid variable name.
right_brace_pattern = r'(?<!{[A-Za-z_][\w.]+)}'
# Escape non-variable-enclosing braces with double braces.
return re.sub(
right_brace_pattern, '}}',
re.sub(
left_brace_pattern, '{{', template
)
)
def extract_placeholders(s: str) -> set: def extract_placeholders(s: str) -> set:
'''Extracts placeholder variables in the format `{variable}` from '''Extracts placeholder variables in the format `{variable}` from
an unformatted template string.''' an unformatted template string.'''
# Regex pattern to match placeholders with alphanumerics, dots, and underscores. # Regex pattern to match placeholders with alphanumerics, dots, and underscores.
placeholder_pattern = r'\{([\w\.]+)\}' placeholder_pattern = r'\{([A-Za-z_][\w.]+)\}'
# Escape braces that are not used to enclose Python variables
# (e.g. those used in inline CSS or JavaScript)
escaped = escape_braces(s)
# Find all matches in the string. # Find all matches in the string.
matches = re.findall(placeholder_pattern, s) matches = re.findall(placeholder_pattern, escaped)
# Return the set of distinct placeholders. # Return the set of distinct placeholders.
return set(matches) return set(matches)
@ -85,7 +119,19 @@ def format_html_template(template: str, **kwargs) -> str:
# there are no more placeholders. The loop is used to account for nested template references. # there are no more placeholders. The loop is used to account for nested template references.
formatted_html = template formatted_html = template
while len(extract_placeholders(formatted_html)) > 0: while len(extract_placeholders(formatted_html)) > 0:
formatted_html = formatted_html.format(globalvars = GlobalVars(), **kwargs)
# Escape non-variable-enclosing braces each iteration of the
# loop, because each time they'll be replaced with single braces.
try:
escaped = escape_braces(formatted_html)
formatted_html = escaped.format(
globalvars = GlobalVars(),
**kwargs
)
except Exception as e:
logger.error('Encountered an exception while formatting the following:')
logger.error(escaped)
raise e
# Return the formatted HTML. # Return the formatted HTML.
return formatted_html return formatted_html

View File

@ -4,3 +4,5 @@ pyyaml
rfeed rfeed
dotmap dotmap
pydantic pydantic
pygments
pymdownextensions

75
site/assets/css/codehilite.css vendored Normal file
View File

@ -0,0 +1,75 @@
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; }
.codehilite .hll { background-color: #ffffcc }
.codehilite { background: #f8f8f8; }
.codehilite .c { color: #3D7B7B; font-style: italic } /* Comment */
.codehilite .err { border: 1px solid #F00 } /* Error */
.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
.codehilite .o { color: #666 } /* Operator */
.codehilite .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #9C6500 } /* Comment.Preproc */
.codehilite .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
.codehilite .gd { color: #A00000 } /* Generic.Deleted */
.codehilite .ge { font-style: italic } /* Generic.Emph */
.codehilite .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.codehilite .gr { color: #E40000 } /* Generic.Error */
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.codehilite .gi { color: #008400 } /* Generic.Inserted */
.codehilite .go { color: #717171 } /* Generic.Output */
.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.codehilite .gs { font-weight: bold } /* Generic.Strong */
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.codehilite .gt { color: #04D } /* Generic.Traceback */
.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.codehilite .kp { color: #008000 } /* Keyword.Pseudo */
.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.codehilite .kt { color: #B00040 } /* Keyword.Type */
.codehilite .m { color: #666 } /* Literal.Number */
.codehilite .s { color: #BA2121 } /* Literal.String */
.codehilite .na { color: #687822 } /* Name.Attribute */
.codehilite .nb { color: #008000 } /* Name.Builtin */
.codehilite .nc { color: #00F; font-weight: bold } /* Name.Class */
.codehilite .no { color: #800 } /* Name.Constant */
.codehilite .nd { color: #A2F } /* Name.Decorator */
.codehilite .ni { color: #717171; font-weight: bold } /* Name.Entity */
.codehilite .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
.codehilite .nf { color: #00F } /* Name.Function */
.codehilite .nl { color: #767600 } /* Name.Label */
.codehilite .nn { color: #00F; font-weight: bold } /* Name.Namespace */
.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
.codehilite .nv { color: #19177C } /* Name.Variable */
.codehilite .ow { color: #A2F; font-weight: bold } /* Operator.Word */
.codehilite .w { color: #BBB } /* Text.Whitespace */
.codehilite .mb { color: #666 } /* Literal.Number.Bin */
.codehilite .mf { color: #666 } /* Literal.Number.Float */
.codehilite .mh { color: #666 } /* Literal.Number.Hex */
.codehilite .mi { color: #666 } /* Literal.Number.Integer */
.codehilite .mo { color: #666 } /* Literal.Number.Oct */
.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
.codehilite .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
.codehilite .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
.codehilite .sx { color: #008000 } /* Literal.String.Other */
.codehilite .sr { color: #A45A77 } /* Literal.String.Regex */
.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
.codehilite .ss { color: #19177C } /* Literal.String.Symbol */
.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
.codehilite .fm { color: #00F } /* Name.Function.Magic */
.codehilite .vc { color: #19177C } /* Name.Variable.Class */
.codehilite .vg { color: #19177C } /* Name.Variable.Global */
.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
.codehilite .vm { color: #19177C } /* Name.Variable.Magic */
.codehilite .il { color: #666 } /* Literal.Number.Integer.Long */

View File

@ -1,3 +1,5 @@
/* TODO: Reorganize CSS */
body{ body{
background: inherit; background: inherit;
background-color: white; background-color: white;
@ -293,3 +295,13 @@ p.socials{
margin-left: 0.25em; margin-left: 0.25em;
margin-right:0.25em; margin-right:0.25em;
} }
article ul, article ol{
margin-bottom: 1em;
}
pre:has(> code), mark{
font-family: 'Oxygen Mono', sans-serif;
font-display: swap;
background-color: #d8d8d8;
}

View File

@ -1,9 +0,0 @@
<article>
<h1>404 Error</h1>
<hr />
<span id="404-message"></span>
<script>
var query_page = $("[name='query_page']")[0].innerHTML;
document.getElementById("404-message").innerHTML="Page '"+query_page+"' not found.";
</script>
</article>

View File

@ -1,6 +0,0 @@
<article>
<h1>About</h1>
<hr />
Coming soon!
</article>

View File

@ -12,9 +12,9 @@
{article.content} {article.content}
<br /><hr /><br /> <br /><hr /><br />
<p> <p>
<a href="{previous_article.path}" title="{previous_article.metadata.title}" data="{previous_article.path}" class="previous-article-link">&lt; Previous</a> <a href="{site.base_url}/{previous_article.path}" title="{previous_article.metadata.title}" data="{previous_article.path}" class="previous-article-link">&lt; Previous</a>
&nbsp; <!-- This non-breaking space prevents the RSS icon from getting weird when previous-article-link is hidden. --> &nbsp; <!-- This non-breaking space prevents the RSS icon from getting weird when previous-article-link is hidden. -->
<a href="{next_article.path}" title="{next_article.metadata.title}" data="{next_article.path}" class="next-article-link">Next &gt;</a> <a href="{site.base_url}/{next_article.path}" title="{next_article.metadata.title}" data="{next_article.path}" class="next-article-link">Next &gt;</a>
</p> </p>
<p>Tags: {blog_tags}{templates.components.rss_icon}</p> <p>Tags: {blog_tags}{templates.components.rss_icon}</p>
</article> </article>

View File

@ -0,0 +1,20 @@
<article>
<span class="first-published-label">First published: </span>
<time class="first-published-date" pubdate data="{article.metadata.date}" datetime="{article.metadata.date}">{article.metadata.date}</time>
<span class="last-modified-label"> · Last modified: </span>
<time class="last-modified-date" data="{article.metadata.lastmod}" datetime="{article.metadata.lastmod}">{article.metadata.lastmod}</time>
<hr />
<h1 class="headline">{article.metadata.title}</h1>
<p class="byline">
<address class="author">By <a rel="author" href="mailto:admin@jimlab.io">Jim Shepich III</a></address>
</p>
{article.content}
<br /><hr /><br />
<p>
<!-- TODO: highlightjs.org -->
<a href="{site.base_url}/{previous_article.path}" title="{previous_article.metadata.title}" data="{previous_article.path}" class="previous-article-link">&lt; Previous</a>
&nbsp; <!-- This non-breaking space prevents the RSS icon from getting weird when previous-article-link is hidden. -->
<a href="{site.base_url}/{next_article.path}" title="{next_article.metadata.title}" data="{next_article.path}" class="next-article-link">Next &gt;</a>
</p>
<p>Tags: {blog_tags}{templates.components.rss_icon}</p>
</article>

View File

@ -1,5 +1,6 @@
<link rel="stylesheet" type="text/css" href="/assets/css/reset.css"> <link rel="stylesheet" type="text/css" href="/assets/css/reset.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Jacquard+12|Fira+Sans|UnifrakturMaguntia"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Jacquard+12|Fira+Sans|UnifrakturMaguntia|Oxygen+Mono">
<link rel="stylesheet" type="text/css" href="/assets/css/common.css"> <link rel="stylesheet" type="text/css" href="/assets/css/common.css">
<link rel="stylesheet" type="text/css" href="/assets/css/layout.css"> <link rel="stylesheet" type="text/css" href="/assets/css/layout.css">
<link rel="stylesheet" type="text/css" href="/assets/css/theme.css"> <link rel="stylesheet" type="text/css" href="/assets/css/theme.css">
<link rel="stylesheet" type="text/css" href="/assets/css/codehilite.css">

View File

@ -1,5 +1,4 @@
<footer> <footer>
<!-- Add socials (email, Matrix, Friendica, Gitea)-->
<p class="socials"> <p class="socials">
<a class="social" href="https://matrix.to/#/@jim:jimlab.io" title="Matrix"><img src="/assets/img/matrix.svg" alt="Matrix logo"></a> <a class="social" href="https://matrix.to/#/@jim:jimlab.io" title="Matrix"><img src="/assets/img/matrix.svg" alt="Matrix logo"></a>
<a class="social" href="https://friendica.jimlab.io/profile/jim/profile" title="Friendica"><img src="/assets/img/friendica.svg" alt="Friendica logo"></a> <a class="social" href="https://friendica.jimlab.io/profile/jim/profile" title="Friendica"><img src="/assets/img/friendica.svg" alt="Friendica logo"></a>
@ -7,4 +6,5 @@
<a class="social" href="mailto:admin@jimlab.io" title="eMail"><img src="/assets/img/email.svg" alt="eMail logo"></a> <a class="social" href="mailto:admin@jimlab.io" title="eMail"><img src="/assets/img/email.svg" alt="eMail logo"></a>
</span> </span>
<br /><p class='copyright'>Copyright &copy; 2021-{globalvars.today.year} Jim Shepich</span> <br /><p class='copyright'>Copyright &copy; 2021-{globalvars.today.year} Jim Shepich</span>
<!-- TODO: Add licenses for 3rd party content (e.g. logos) -->
</footer> </footer>

View File

@ -2,4 +2,5 @@
<a href='/index.html' target='_self'><div class='nav-tab'><span class='nav-text'>Home</span></div></a> <a href='/index.html' target='_self'><div class='nav-tab'><span class='nav-text'>Home</span></div></a>
<a href='/shepich_resume.pdf' target='_blank'><div class='nav-tab'><span class='nav-text'>Resume</span></div></a> <a href='/shepich_resume.pdf' target='_blank'><div class='nav-tab'><span class='nav-text'>Resume</span></div></a>
<a href='/dogma-jimfinium/archive.html' target='_self'><div class='nav-tab'><span class='nav-text'>Dogma Jimfinium</span></div></a> <a href='/dogma-jimfinium/archive.html' target='_self'><div class='nav-tab'><span class='nav-text'>Dogma Jimfinium</span></div></a>
<a href='/jimoire-laboratoire/index.html' target='_self'><div class='nav-tab'><span class='nav-text'>Jimoire Laboratoire</span></div></a>
</nav> </nav>