refactor #1

Merged
jim merged 20 commits from refactor into main 2026-02-03 01:08:46 -05:00
15 changed files with 361 additions and 76 deletions
Showing only changes of commit f933e47a04 - Show all commits

View File

@ -1,12 +1,8 @@
author: Jim Shepich III
templates_folder: ./templates
site_defaults:
templates:
partials: ./templates/partials
components: ./templates/components
pages: ./templates/pages
templates_folder: ./site/templates
sites:
main:
title: Jimlab
base_url: http://localhost:8000
web_root: ./dist
build_cache: ./site
@ -14,14 +10,20 @@ sites:
- /assets
articles:
- ./pages/*.md
template_selections:
article: templates.components.simple_article
resume:
title: Resume
base_url: http://localhost:8000
web_root: ./dist
git_repo: ssh://gitea/jim/resume.git
build_cache: ./build/resume
assets:
- 'shepich_resume.pdf'
dogma_jimfinium:
title: Dogma Jimfinium
base_url: http://localhost:8080/dogma-jimfinium
git_repo: ssh://gitea/jim/dogma-jimfinium.git
build_cache: ./build/dogma-jimfinium

View File

@ -10,11 +10,11 @@ from typing import Optional
from datetime import datetime, date
from dotmap import DotMap
from .common import filepath_or_string, GlobalVars, SiteConfig
from .templating import format_html_template, map_templates
from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access
from .templating import format_html_template, map_templates, TemplateSelections
from .assets import pull_git_repo, copy_assets
from .articles import ArticleMetadata, load_markdown, build_articles, build_index
from .blog import build_blog_archive, build_rss_feed
@ -25,6 +25,10 @@ from .articles import ArticleMetadata, load_markdown, build_articles, build_inde
def build_site(site: SiteConfig, templates: DotMap):
# Do not build a site marked as unpublished.
if not site.published:
return None
# Initialize the build cache and web root, in case they do not exist.
os.makedirs(site.build_cache, exist_ok = True)
os.makedirs(site.web_root, exist_ok = True)
@ -39,8 +43,15 @@ def build_site(site: SiteConfig, templates: DotMap):
# Load the site's articles into an index.
index = build_index(site)
# Determine which templates are to be used for explicit applications, e.g.
# the tag component.
template_selections = TemplateSelections(site, templates)
# Generate HTML pages for the articles.
build_articles(site, index, templates)
build_articles(site, index, templates, template_selections)
if len(site.articles or []):
build_blog_archive(site, index, template_selections, templates = templates)
def main():

View File

@ -8,7 +8,7 @@ from dotmap import DotMap
from datetime import date
from .common import filepath_or_string, SiteConfig
from .templating import format_html_template
from .templating import format_html_template, TemplateSelections
class ArticleMetadata(pydantic.BaseModel):
title: str
@ -70,30 +70,39 @@ def build_index(site: SiteConfig) -> dict:
return index
def format_article_tags(tags: list[str], template = 'templates/components/blog_tag.html') -> list[str]:
def format_article_tags(tags: list[str], tag_template, **kwargs) -> list[str]:
'''Generates HTML article tag components from a list of tag names.'''
return [
format_html_template(template, tag_name = t) for t in tags
format_html_template(tag_template, tag_name = t, **kwargs) for t in tags
]
def build_articles(site: SiteConfig, index: dict[str, tuple[ArticleMetadata, str]], templates: DotMap):
def build_articles(
site: SiteConfig,
index: dict[str, tuple[ArticleMetadata, str]],
templates: DotMap,
template_selections: TemplateSelections
):
'''Generates HTML files for all of a given site's Markdown articles
by interpolating the contents and metadata into the HTML templates.'''
for filestem, (metadata, content) in index.items():
article = format_html_template(
templates.components.blog_article,
template_selections['article'],
content = content,
blog_tags = ' '.join(format_article_tags(metadata.tags)),
blog_tags = ' '.join(format_article_tags(
metadata.tags, template_selections['tag'], site = site
)),
metadata = metadata,
templates = templates
templates = templates,
site = site
)
page = format_html_template(
templates.pages.default,
template_selections['page'],
content = article,
templates = templates
templates = templates,
site = site
)

View File

@ -4,14 +4,13 @@ import datetime
from .common import SiteConfig
from .articles import ArticleMetadata, format_article_tags
from .templating import format_html_template
from .templating import format_html_template, TemplateSelections
def build_blog_archive(
site: SiteConfig,
index: dict[str, tuple[str, str]],
page_template = 'templates/pages/default.html',
li_template = 'templates/components/blog_archive_li.html',
template_selections: TemplateSelections,
**kwargs
) -> str:
'''Converts an index, formatted as filestem: (metadata, contents) dict,
@ -22,27 +21,50 @@ def build_blog_archive(
'''
# Add each article as a list item to an unordered list.
archive_html_content = '<ul>'
archive_html_list = '<ul>'
for article, (metadata, contents) in sorted(index.items(), key = lambda item: item[1][0].date)[::-1]:
# Generate HTML for the article (including metadata tags).
archive_html_content += format_html_template(
li_template,
archive_html_list += format_html_template(
template_selections['archive_li'],
article_filestem = article,
blog_tags = ' '.join(format_article_tags(metadata.tags)),
metadata = metadata
blog_tags = ' '.join(format_article_tags(metadata.tags, template_selections['tag'], site = site)),
metadata = metadata,
site = site,
**kwargs
)
archive_html_content +='</ul>'
archive_html_list +='</ul>'
# Interpolate the article into the overall page template.
archive_html_page = format_html_template(
page_template,
content = archive_html_content,
# Generate the archive article.
archive_html_article = format_html_template(
template_selections['archive_article'],
content = archive_html_list,
site = site,
**kwargs
)
return archive_html_page
# Interpolate the article into the overall page template.
archive_html_page = format_html_template(
template_selections['page'],
content = archive_html_article,
site = site,
**kwargs
)
with open(f'{site.web_root.rstrip('/')}/archive.html', 'w') as f:
f.write(archive_html_page)
def build_tag_reference(index: dict[str, tuple[str, str]]):
tag_index = {}
for article, (metadata, content) in index.items():
for tag in metadata.tags:
tag_index[tag] = (tag_index.get(tag,[])) + [article]
for tag, articles in tag_index.items():
pass
# TODO: Finish
def build_rss_feed(site: SiteConfig, index: dict[str, tuple[ArticleMetadata, str]]):

View File

@ -1,6 +1,8 @@
import os
import inspect
import subprocess
import pydantic
from dotmap import DotMap
from typing import Optional
from datetime import date, datetime
@ -28,6 +30,42 @@ class SiteConfig(pydantic.BaseModel):
base_url: str
web_root: str
build_cache: str
title: str
published: Optional[bool] = True
git_repo: Optional[str] = None
assets: Optional[list] = None
articles: Optional[list] = None
template_selections: Optional[dict] = {}
def get_var_names(var):
'''Gets the name(s) of an input variable.
From https://stackoverflow.com/questions/18425225/getting-the-name-of-a-variable-as-a-string.'''
callers_local_vars = inspect.currentframe().f_back.f_back.f_locals.items()
return [var_name for var_name, var_val in callers_local_vars if var_val is var]
def dotmap_access(d: DotMap, s: str):
'''Facilitates nested subscripting into a DotMap using a
string containing period-delimited keys.
# Example
```python
dd = DotMap({'a':{'b':{'c':42}}})
dotmap_access(dd, 'dd.a.b.c')
>>> 42
```
'''
keys = s.split('.')
# If the string starts with the dotmap's name, ignore it.
if keys[0] in get_var_names(d):
keys.pop(0)
# Iteratively subscript into the nested dotmap until it's done.
result = d
for k in keys:
result = result[k]
return result

View File

@ -1,7 +1,7 @@
import os
import re
from dotmap import DotMap
from .common import filepath_or_string, GlobalVars
from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access
def extract_placeholders(s: str) -> set:
@ -121,3 +121,32 @@ def map_templates(dir: str, parent = '') -> DotMap:
output[filestem] = html
return DotMap(output)
# TODO: Come up with a more consistent naming scheme for defaults.
class TemplateSelections:
'''An interface for retrieving templates that are explicitly used in the
compiler, such as the article and tag components, as well as the default page.
Relies on `SiteConfig.template_selection` for custom overrides, falling back on
`config[template_default_selections]`, and finally on defaults hard-coded into
the definition of this class.
'''
def __init__(self, site: SiteConfig, templates: DotMap, config: dict = None):
self.site = site
self.templates = templates
self.defaults = dict(
tag = 'templates.components.blog_tag',
article = 'templates.components.blog_article',
page = 'templates.pages.default',
archive_li = 'templates.components.blog_archive_li',
archive_article = 'templates.components.blog_archive'
) | (config or {}).get('template_default_selections', {})
def __getitem__(self, key: str) -> str:
# Templates variable must be named `templates` for dotmap_access to work correctly.
templates = self.templates
return dotmap_access(templates, self.site.template_selections.get(key, self.defaults[key]))

129
site/assets/img/favicon.svg Normal file
View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="190.6763mm"
height="190.67599mm"
viewBox="0 0 190.67628 190.676"
version="1.1"
id="svg1"
sodipodi:docname="jimlab.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.46774"
inkscape:cx="153.93167"
inkscape:cy="366.65669"
inkscape:window-width="1280"
inkscape:window-height="727"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<linearGradient
id="linearGradient18"
inkscape:collect="always">
<stop
style="stop-color:#666666;stop-opacity:1;"
offset="0"
id="stop19" />
<stop
style="stop-color:#d9d9d9;stop-opacity:1;"
offset="1"
id="stop20" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient18"
id="linearGradient20"
x1="109.12461"
y1="131.25221"
x2="92.506081"
y2="321.20288"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-5.4771806,-130.88957)">
<path
id="rect1-6-0-6"
style="display:inline;fill:url(#linearGradient20);fill-opacity:1;stroke:none;stroke-width:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
inkscape:label="hourglass"
d="m 10.305609,180.9727 45.2546,45.25461 -45.254596,45.25512 h 90.509717 90.50972 l -45.2546,-45.25512 45.2546,-45.2546 -90.50921,-1e-5 h -5.1e-4 -5.2e-4 z" />
<g
id="g14"
inkscape:label="blue-diamond"
transform="rotate(45,-14.34779,124.80922)"
style="display:inline">
<rect
style="display:inline;fill:#39b8ab;fill-opacity:1;stroke:none;stroke-width:1.54489;stroke-dasharray:none;stroke-opacity:1"
id="rect1-6-0"
width="64"
height="64"
x="106.79844"
y="83.090195"
inkscape:label="big-trim" />
<rect
style="display:inline;fill:#47edda;fill-opacity:1;stroke:none;stroke-width:1.44834;stroke-dasharray:none;stroke-opacity:1"
id="rect1-6"
width="60"
height="60"
x="108.79844"
y="85.090195"
inkscape:label="big-fill" />
<rect
style="display:inline;fill:#39b8ab;fill-opacity:1;stroke:none;stroke-width:1.5831;stroke-dasharray:none;stroke-opacity:1"
id="rect1-6-7-0"
width="32"
height="32.000004"
x="122.79843"
y="99.090187"
inkscape:label="small-trim" />
<rect
style="display:inline;fill:#47edda;fill-opacity:1;stroke:none;stroke-width:1.3852;stroke-dasharray:none;stroke-opacity:1"
id="rect1-6-7"
width="28"
height="28.000002"
x="124.79843"
y="101.09019"
inkscape:label="small-fill" />
<path
style="fill:none;fill-opacity:1;stroke:#39b8ab;stroke-width:2.2595;stroke-dasharray:none;stroke-opacity:1"
d="m 108.89019,85.181941 15.81649,15.816499"
id="path11"
inkscape:label="top-trim" />
<path
style="fill:none;fill-opacity:1;stroke:#39b8ab;stroke-width:2.16775;stroke-dasharray:none;stroke-opacity:1"
d="M 153.56485,100.32377 168.73912,85.149504"
id="path12"
inkscape:label="right-trim" />
<path
style="fill:none;fill-opacity:1;stroke:#39b8ab;stroke-width:1.77778;stroke-dasharray:none;stroke-opacity:1"
d="m 152.79843,129.09019 16,16"
id="path13"
inkscape:label="bottom-trim" />
<path
style="fill:none;fill-opacity:1;stroke:#39b8ab;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
d="m 124.79843,129.09019 -15.99999,16"
id="path14"
inkscape:label="left-trim" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -1,22 +0,0 @@
<html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
{templates.partials.default_css}
{templates.partials.header}
{templates.partials.nav}
</head>
<body>
<main>
<article>
<h1 class="headline">{metadata.title}</h1>
<p class="byline">
<address class="author">By <a rel="author" href="mailto:admin@jimlab.io">Jim Shepich III</a></address>
<br/>First published: <time pubdate datetime="{metadata.date}">{metadata.date}</time>
<br/>Last modified: <time pubdate datetime="{metadata.lastmod}">{metadata.lastmod}</time>
</p>
{content}
</article>
</main>
{templates.partials.footer}
</body>

View File

@ -0,0 +1,5 @@
<article>
<h1 class="headline">{site.title} Archive</h1>
<hr />
{content}
</article>

View File

@ -1 +1 @@
<a href='tags/{tag_name}.html'><span class='blog-tag'>{tag_name}</span></a>
<a href='{site.base_url}/tags/{tag_name}.html'><span class='blog-tag'>{tag_name}</span></a>

View File

@ -0,0 +1,5 @@
<article>
<h1 class="headline">{metadata.title}</h1>
<hr />
{content}
</article>

View File

@ -5,6 +5,8 @@
{templates.partials.default_css}
{templates.partials.header}
{templates.partials.nav}
<title>{site.title}</title>
<link rel="icon" type="image/svg" href="/assets/img/favicon.svg">
</head>
<body>
<main>

View File

@ -1,5 +1,5 @@
<nav id="main-navbar" class="no-highlight">
<a href='/home.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/index.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>
</nav>

View File

@ -1,14 +0,0 @@
<html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
{templates.partials.default_css}
{templates.partials.header}
{templates.partials.nav}
</head>
<body>
<main>
{content}
</main>
{templates.partials.footer}
</body>

View File

@ -25,6 +25,14 @@
"from jimsite import *"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "68b107f1",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": 3,
@ -120,13 +128,74 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 4,
"id": "a28b95a6",
"metadata": {},
"outputs": [],
"outputs": [
{
"ename": "NameError",
"evalue": "name 'build_site' is not defined",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43mbuild_site\u001b[49m(sites[\u001b[33m'\u001b[39m\u001b[33mresume\u001b[39m\u001b[33m'\u001b[39m])\n",
"\u001b[31mNameError\u001b[39m: name 'build_site' is not defined"
]
}
],
"source": [
"build_site(sites['resume'])"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "60378277",
"metadata": {},
"outputs": [],
"source": [
"import inspect"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cd7fb9f3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"DotMap(a=DotMap(b=DotMap(c=42)))\n",
"a\n",
"DotMap(b=DotMap(c=42))\n",
"b\n",
"DotMap(c=42)\n",
"c\n"
]
},
{
"data": {
"text/plain": [
"42"
]
},
"execution_count": 29,
"metadata": {},
"output_type": "execute_result"
}
],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "fcd2faf5",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {