Compare commits

..

2 Commits

18 changed files with 248 additions and 56 deletions

3
.gitattributes vendored
View File

@ -2,4 +2,5 @@
* text=auto
# Ignore Jupyter Notebooks in language stats.
*.ipynb linguist-vendored
*.ipynb linguist-vendored
site/assets/css/codehilite.css linguist-vendored

View File

@ -8,18 +8,11 @@ sites:
build_cache: ./site
assets:
- /assets
- robots.txt
articles:
- ./pages/*.md
- '*.md'
template_selections:
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:
title: Resume
@ -50,6 +43,20 @@ sites:
addons:
- 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

View File

@ -5,6 +5,7 @@ import logging
logger = logging.getLogger(__name__)
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 .templating import format_html_template, map_templates, TemplateSelections

View File

@ -2,7 +2,10 @@ import os
import glob
import yaml
import markdown
from pygments.formatters import HtmlFormatter
from markdown.extensions.codehilite import CodeHiliteExtension
import pydantic
import logging
from typing import Optional, Union
from dotmap import DotMap
from datetime import date
@ -10,12 +13,37 @@ from datetime import date
from .common import filepath_or_string, SiteConfig
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):
'''A model for the YAML frontmatter included with Markdown articles.'''
title: str
date: date
published: bool
tags: list
tags: Optional[list] = []
published: Optional[bool] = False
author: Optional[str] = None
lastmod: Optional[date] = None
thumbnail: Optional[str] = None
@ -41,12 +69,14 @@ def load_markdown(md: str) -> tuple[Optional[ArticleMetadata], str]:
# Split the metadata from the contents.
[raw_metadata, raw_article] = md.split('---')
# TODO: Consider adding an optional postscript document, for things like unescaped CSS.
# Use YAML to parse the metadata.
metadata = yaml.safe_load(raw_metadata)
# Convert the contents to a HTML string.
content = markdown.markdown(raw_article)
content = markdown.markdown(raw_article, extensions = MARKDOWN_EXTENSIONS)
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)
# 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.
if not metadata.published:
logger.debug(f'Article loaded from {article_full_path} is not published. Skipping.')
continue
# 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')
# TODO: add tolerance for a hierarchical article directory structure.
article_path = os.path.basename(article_full_path).replace('.md', '.html')
# Construct the article's path for the index by discarding the build cache.
article_path = article_full_path.removeprefix(site.build_cache).lstrip("/").replace('.md','.html')
index[article_path] = Article(
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)

View File

@ -1,8 +1,10 @@
import os
import glob
import shutil
import logging
from .common import run, GitRepo, SiteConfig
logger = logging.getLogger(__name__)
def pull_git_repo(repo: GitRepo, build_cache: str) -> None:
'''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("/")}')
)
for asset in expanded_asset_list:
for asset in set(expanded_asset_list):
# Skip ignored assets.
if asset in ignored_asset_list:

View File

@ -58,9 +58,9 @@ def build_blog_archive(
tag_selector_options = []
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;
}}}}
}}
''']
# Add tag selector options in descending order of article count.
@ -75,12 +75,12 @@ def build_blog_archive(
if tag == '*':
continue
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;
}}}}
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;
}}}}
}}
''')
# For Python 3.9-friendliness.

View File

@ -1,18 +1,52 @@
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
logger = logging.getLogger(__name__)
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:
'''Extracts placeholder variables in the format `{variable}` from
an unformatted template string.'''
# 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.
matches = re.findall(placeholder_pattern, s)
matches = re.findall(placeholder_pattern, escaped)
# Return the set of distinct placeholders.
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.
formatted_html = template
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 formatted_html

View File

@ -3,4 +3,6 @@ markdown
pyyaml
rfeed
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{
background: inherit;
background-color: white;
@ -292,4 +294,14 @@ p.socials{
filter: brightness(0) saturate(100%) invert(95%) sepia(100%) saturate(14%) hue-rotate(213deg) brightness(104%) contrast(104%);
margin-left: 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}
<br /><hr /><br />
<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. -->
<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>Tags: {blog_tags}{templates.components.rss_icon}</p>
</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" 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/layout.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>
<!-- Add socials (email, Matrix, Friendica, Gitea)-->
<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://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>
</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>

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='/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='/jimoire-laboratoire/index.html' target='_self'><div class='nav-tab'><span class='nav-text'>Jimoire Laboratoire</span></div></a>
</nav>