From f933e47a0427cdd3e639c4d05c93c20c7769f06e Mon Sep 17 00:00:00 2001 From: Jim Shepich III Date: Sun, 1 Feb 2026 14:10:32 -0500 Subject: [PATCH] Standardizing templating --- config.yaml | 14 +- jimsite/__init__.py | 19 ++- jimsite/articles.py | 27 ++-- jimsite/blog.py | 50 +++++-- jimsite/common.py | 40 +++++- jimsite/templating.py | 33 ++++- site/assets/img/favicon.svg | 129 ++++++++++++++++++ site/templates/blog_post.html | 22 --- site/templates/components/blog_archive.html | 5 + site/templates/components/blog_tag.html | 2 +- site/templates/components/simple_article.html | 5 + site/templates/pages/default.html | 2 + site/templates/partials/nav.html | 2 +- site/templates/simple.html | 14 -- testbench.ipynb | 73 +++++++++- 15 files changed, 361 insertions(+), 76 deletions(-) create mode 100644 site/assets/img/favicon.svg delete mode 100644 site/templates/blog_post.html create mode 100644 site/templates/components/blog_archive.html create mode 100644 site/templates/components/simple_article.html delete mode 100644 site/templates/simple.html diff --git a/config.yaml b/config.yaml index 8065da7..01715d9 100644 --- a/config.yaml +++ b/config.yaml @@ -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 diff --git a/jimsite/__init__.py b/jimsite/__init__.py index 8572ae2..b07106c 100644 --- a/jimsite/__init__.py +++ b/jimsite/__init__.py @@ -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(): diff --git a/jimsite/articles.py b/jimsite/articles.py index 01ef1de..9ee38d3 100644 --- a/jimsite/articles.py +++ b/jimsite/articles.py @@ -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 ) diff --git a/jimsite/blog.py b/jimsite/blog.py index 806845b..bbefe98 100644 --- a/jimsite/blog.py +++ b/jimsite/blog.py @@ -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 = '' + + # Generate the archive article. + archive_html_article = format_html_template( + template_selections['archive_article'], + content = archive_html_list, + site = site, + **kwargs + ) # Interpolate the article into the overall page template. archive_html_page = format_html_template( - page_template, - content = archive_html_content, + template_selections['page'], + content = archive_html_article, + site = site, **kwargs ) - return archive_html_page + 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]]): diff --git a/jimsite/common.py b/jimsite/common.py index 6f129de..9bc82ed 100644 --- a/jimsite/common.py +++ b/jimsite/common.py @@ -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 \ No newline at end of file + 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 diff --git a/jimsite/templating.py b/jimsite/templating.py index f05c332..3a1dfd9 100644 --- a/jimsite/templating.py +++ b/jimsite/templating.py @@ -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: @@ -120,4 +120,33 @@ def map_templates(dir: str, parent = '') -> DotMap: output[filestem] = html - return DotMap(output) \ No newline at end of file + 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])) + diff --git a/site/assets/img/favicon.svg b/site/assets/img/favicon.svg new file mode 100644 index 0000000..e33690b --- /dev/null +++ b/site/assets/img/favicon.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/templates/blog_post.html b/site/templates/blog_post.html deleted file mode 100644 index 6322b55..0000000 --- a/site/templates/blog_post.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - {templates.partials.default_css} - {templates.partials.header} - {templates.partials.nav} - - -
-
-

{metadata.title}

-
By
-
First published: -
Last modified: -

- {content} -
-
- {templates.partials.footer} - \ No newline at end of file diff --git a/site/templates/components/blog_archive.html b/site/templates/components/blog_archive.html new file mode 100644 index 0000000..0c2033b --- /dev/null +++ b/site/templates/components/blog_archive.html @@ -0,0 +1,5 @@ +
+

{site.title} Archive

+
+ {content} +
\ No newline at end of file diff --git a/site/templates/components/blog_tag.html b/site/templates/components/blog_tag.html index 1a77d3b..8f1ebaa 100644 --- a/site/templates/components/blog_tag.html +++ b/site/templates/components/blog_tag.html @@ -1 +1 @@ -{tag_name} \ No newline at end of file +{tag_name} \ No newline at end of file diff --git a/site/templates/components/simple_article.html b/site/templates/components/simple_article.html new file mode 100644 index 0000000..0b33bfe --- /dev/null +++ b/site/templates/components/simple_article.html @@ -0,0 +1,5 @@ +
+

{metadata.title}

+
+ {content} +
\ No newline at end of file diff --git a/site/templates/pages/default.html b/site/templates/pages/default.html index 5753822..66608ca 100644 --- a/site/templates/pages/default.html +++ b/site/templates/pages/default.html @@ -5,6 +5,8 @@ {templates.partials.default_css} {templates.partials.header} {templates.partials.nav} + {site.title} +
diff --git a/site/templates/partials/nav.html b/site/templates/partials/nav.html index 1c8861f..ed5c746 100644 --- a/site/templates/partials/nav.html +++ b/site/templates/partials/nav.html @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/site/templates/simple.html b/site/templates/simple.html deleted file mode 100644 index 5753822..0000000 --- a/site/templates/simple.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - {templates.partials.default_css} - {templates.partials.header} - {templates.partials.nav} - - -
- {content} -
- {templates.partials.footer} - \ No newline at end of file diff --git a/testbench.ipynb b/testbench.ipynb index ceb9bdb..72d680d 100644 --- a/testbench.ipynb +++ b/testbench.ipynb @@ -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": {