From f0b26fb2d5f0bca3ceb5c6ffae4d63fed7ec62a6 Mon Sep 17 00:00:00 2001 From: Jim Shepich III Date: Sat, 31 Jan 2026 14:02:22 -0500 Subject: [PATCH] Added true recursive template interpolation and cleaned up --- config.yaml | 2 +- jimsite.py | 92 +++++++-- testbench.ipynb | 512 ++---------------------------------------------- 3 files changed, 96 insertions(+), 510 deletions(-) diff --git a/config.yaml b/config.yaml index aaf4501..7a5e5ed 100644 --- a/config.yaml +++ b/config.yaml @@ -18,7 +18,7 @@ sites: git_repo: ssh://gitea/jim/resume.git build_cache: ./build/resume assets: - - '{build_cache}/shepich_resume.pdf' + - 'shepich_resume.pdf' dogma_jimfinium: base_url: http://localhost:8080/dogma-jimfinium git_repo: ssh://gitea/jim/dogma-jimfinium.git diff --git a/jimsite.py b/jimsite.py index d18dfad..f8e3fbc 100644 --- a/jimsite.py +++ b/jimsite.py @@ -1,4 +1,5 @@ import os +import re import glob import shutil import subprocess @@ -23,6 +24,59 @@ def filepath_or_string(s: str) -> str: return s +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\.]+)\}' + + # Find all matches in the string. + matches = re.findall(placeholder_pattern, s) + + # Return the set of distinct placeholders. + return set(matches) + + +def find_cyclical_placeholders(s: str, _parents: tuple = None, _cycles: set = None, **kwargs) -> set[tuple]: + '''Recursively interpolates supplied kwargs into a template string to validate + that there are no cyclical dependencies that would cause infinite recursion. + + Returns a list of paths (expressed as tuples of nodes) of cyclical placeholders. + ''' + + # Track the lineage of each placeholder so we can see if it is its own ancestor. + if _parents is None: + _parents = tuple() + + # Keep track of any cycles encountered. + if _cycles is None: + _cycles = set() + + # Extract the placeholders from the input. + placeholders = extract_placeholders(s) + + # Recursion will naturally end once there are no more nested placeholders. + for p in placeholders: + + # Any placeholder that has itself in its ancestry forms a cycle. + if p in _parents: + _cycles.add(_parents + (p,)) + + # For placeholders that are not their own ancestor, recursively + # interpolate the kwargs into the nested placeholders until we reach + # strings without placeholders. + else: + find_cyclical_placeholders( + ('{'+p+'}').format(**kwargs), + _parents = _parents+(p,), + _cycles = _cycles, + **kwargs + ) + + return _cycles + + with open('config.yaml', 'r') as config_file: config = yaml.safe_load(config_file.read()) @@ -69,20 +123,36 @@ def load_markdown(md: str) -> tuple[ArticleMetadata|None, str]: def format_html_template(template: str, **kwargs) -> str: '''Interpolates variables specified as keyword arguments - into the given HTML template.''' + into the given HTML template. + + # Example + + ```python + kwargs = {'a': '1', 'b': '2', 'c': '{d}+{e}', 'd': '3', 'e': '{c}'} + s = '{a} + {b} = {c}' + find_cyclical_placeholders(s, **kwargs) + + >>> {('c', 'e', 'c')} + ``` + ''' # Load the template if a filepath is given. template = filepath_or_string(template) - # Interpolate the kwargs into the HTML template. - # Apply global variables twice in case a partial used - # by the first call of .format() uses a variable. - html = template.format( - globalvars = GlobalVars(), **kwargs - ).format(globalvars = GlobalVars()) + # Ensure the template does not have cyclical placeholder references. + cycles = find_cyclical_placeholders(template, globalvars = GlobalVars(), **kwargs) + + if len(cycles) > 0: + raise ValueError('Template has cyclical dependencies: {cycles}') + + # Iteratively interpolate global variables and the kwargs into the template until + # 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) # Return the formatted HTML. - return html + return formatted_html run = lambda cmd: subprocess.run(cmd.split(' '), stdout = subprocess.PIPE, stderr = subprocess.PIPE) @@ -212,7 +282,7 @@ def build_index(site: SiteConfig) -> dict: # Expand any globbed expressions. expanded_article_list = [] - for a in site.articles: + for a in site.articles or {}: expanded_article_list.extend( # Article paths are defined relative to the build cache; construct the full path. glob.glob(f'{site.build_cache}/{a.lstrip("/")}') @@ -259,10 +329,6 @@ def map_templates(dir: str, parent = '') -> DotMap: with open(full_path, 'r') as file: html = file.read() - # # Interpolate global variables into partials. - # if 'partials' in full_path: - # html = html.format(globalvars = GlobalVars()) - output[filestem] = html return DotMap(output) diff --git a/testbench.ipynb b/testbench.ipynb index f941874..20c1804 100644 --- a/testbench.ipynb +++ b/testbench.ipynb @@ -1,33 +1,8 @@ { "cells": [ - { - "cell_type": "markdown", - "id": "dda60de8", - "metadata": {}, - "source": [ - "## Roadmap\n", - "\n", - "- [x] Load markdown\n", - "- [] Determine static website structure\n", - " - Where to put assets for subsites like dogma jimfinium\n", - " - How to otherwise organize pages\n", - "- [x] ~~Resolve markdown links~~\n", - "- [] Consider separating article templates and overall page templates\n", - "- [] RSS feed\n", - "\n", - "\n", - "WEBROOT\n", - "- assets\n", - "- main pages\n", - "- resume\n", - "- dogma-jimfinium/\n", - " - assets/\n", - " - pages" - ] - }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "207d2510", "metadata": {}, "outputs": [], @@ -52,390 +27,17 @@ }, { "cell_type": "code", - "execution_count": 19, - "id": "68233fbb", + "execution_count": null, + "id": "8f435a12", "metadata": {}, "outputs": [], "source": [ - "config['templates_folder'] = './templates'\n", - "templates_dict = {}\n", - "for subfolder, _, files in os.walk(config['templates_folder']):\n", - " templates_dict[subfolder] = {}\n", - " for file in files:\n", - " templates_dict[subfolder][file] = 1" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "0f4bf2d6", - "metadata": {}, - "outputs": [ - { - "ename": "KeyError", - "evalue": "'poop'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mKeyError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[36]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[33;43m'\u001b[39;49m\u001b[38;5;132;43;01m{poop}\u001b[39;49;00m\u001b[33;43m \u001b[39;49m\u001b[38;5;132;43;01m{loop}\u001b[39;49;00m\u001b[33;43m'\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mformat\u001b[49m\u001b[43m(\u001b[49m\u001b[43mloop\u001b[49m\u001b[43m \u001b[49m\u001b[43m=\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", - "\u001b[31mKeyError\u001b[39m: 'poop'" - ] - } - ], - "source": [ - "'{poop} {loop}'.format(loop = 1)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "9abb76b2", - "metadata": {}, - "outputs": [], - "source": [ - "def find_cyclical_placeholders(s, _parents = None, _leaves = None, **kwargs):\n", - " if _parents is None:\n", - " _parents = tuple()\n", - " if _leaves is None:\n", - " _leaves = {}\n", - " placeholders = extract_placeholders(s)\n", - "\n", - " if placeholders is None or len(placeholders) == 0:\n", - " _leaves[_parents] = False\n", - "\n", - " for p in placeholders:\n", - " if p in _parents:\n", - " _leaves[_parents + (p,)] = True\n", - " else:\n", - " find_cyclical_placeholders(\n", - " ('{'+p+'}').format(**kwargs),\n", - " _parents = _parents+(p,),\n", - " _leaves = _leaves,\n", - " **kwargs\n", - " )\n", - "\n", - " return _leaves\n", - " \n", - " \n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "8699f542", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{('a',): False, ('b',): False, ('c', 'd'): False, ('c', 'e', 'c'): True}" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "kwargs = {'a': '1', 'b': '2', 'c': '{d}+{e}', 'd': '3', 'e': '{c}'}\n", - "s = '{a} + {b} = {c}'\n", - "find_cyclical_placeholders(s, **kwargs)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "92310172", - "metadata": {}, - "outputs": [], - "source": [ - "def extract_placeholders(s):\n", - " # Regex pattern to match placeholders\n", - " placeholder_pattern = r'\\{(\\w+)\\}'\n", + "with open('config.yaml', 'r') as config_file:\n", + " config = yaml.safe_load(config_file.read())\n", " \n", - " # Find all matches in the string\n", - " matches = re.findall(placeholder_pattern, s)\n", - " \n", - " return matches\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e6884a45", - "metadata": {}, - "outputs": [], - "source": [ + "templates = map_templates(config['templates_folder'])\n", "\n", - "FOR FORMAT_HTML_TEMPLATE:\n", - "unformatted = ...\n", - "formatted = None\n", - "filled_placeholders = set()\n", - "while unformatted != formatted:\n", - " placeholders = extract_paceholders(unformatted)\n", - " formatted = unformatted.format(...)\n", - " filled_placeholders.add(set(placeholders))" - ] - }, - { - "cell_type": "code", - "execution_count": 82, - "id": "9e4becd3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'\\n\\n\\n\\n \\n\\n\\n\\n\\n
\\n Jimlab\\n
\\n \\n\\n\\n
\\n foo\\n
\\n \\n'" - ] - }, - "execution_count": 82, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "format_html_template(templates.simple, content = 'foo', partials = templates.partials)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5344cc93", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "56c5d493", - "metadata": {}, - "outputs": [], - "source": [ - "def build_html_tree(root_dir):\n", - " # Create an empty dictionary to store the HTML files\n", - " html_files = {}\n", - "\n", - " # Walk through the directory tree\n", - " for dirpath, dirs, files in os.walk(root_dir):\n", - " level = root_dir.replace('\\\\', '/').count('/') # Counting slashes to determine depth\n", - " rel_path = dirpath[len(root_dir) + 1:].replace('\\\\', '/') # Get relative path\n", - "\n", - " if not rel_path: # If the path is the root directory, start with a dot for nesting\n", - " html_files['.'] = {}\n", - " rel_path = '.'\n", - " \n", - " current_level = html_files\n", - " \n", - " # Navigate through directories in the relative path\n", - " for part in rel_path.split('/'):\n", - " if part not in current_level:\n", - " current_level[part] = {} # Create a new nested dictionary if it doesn't exist\n", - " current_level = current_level[part]\n", - " \n", - " # Add .html files to the appropriate directory level\n", - " for file in [f for f in files if f.endswith('.html')]:\n", - " current_level[file] = {}\n", - "\n", - " return html_files" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "35dee326", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DotMap(blog_tag.html=DotMap(), blog_article.html=DotMap(), blog_archive_li.html=DotMap(), _ipython_display_=DotMap(), _repr_mimebundle_=DotMap())" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "templates = DotMap(build_html_tree('./templates'))\n", - "templates.components" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "4b17a3ed", - "metadata": {}, - "outputs": [], - "source": [ - "PARTIALS = load_partials()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "d2361c42", - "metadata": {}, - "outputs": [], - "source": [ - "metadata, content = load_markdown('pages/home.md')\n", - "# content = content.replace('src=\"assets', 'src=\"../tmp/dogma-jimfinium/assets')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d718ae33", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "datetime.datetime(2026, 1, 31, 3, 50, 47, 891770)" - ] - }, - "execution_count": 54, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "ed7b3b2f", - "metadata": {}, - "outputs": [], - "source": [ - "html = format_html_template('templates/pages/default.html', content = content, metadata = metadata, **PARTIALS)\n", - "with open('dist/home.html', 'w') as f:\n", - " f.write(html)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b8c87620", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "57383c24", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2025-12-01\n" - ] - }, - { - "data": { - "text/plain": [ - "CompletedProcess(args=['cp', 'build/resume/2025-12-01/shepich_resume.pdf', 'dist/shepich_resume.pdf'], returncode=0, stdout=b'', stderr=b'')" - ] - }, - "execution_count": 170, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "cced61c4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'gear-for-new-parents.md'" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "os.path.basename( 'build/dogma-jimfinium/gear-for-new-parents.md')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "944a5efd", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "132a32ec", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'superlock': (ArticleMetadata(title='Superlock', author='Jim Shepich III', date=datetime.date(2025, 11, 26), lastmod=None, published=True, tags=['martialarts'], thumbnail='assets/images/superlock.jpg'),\n", - " '

The \"Superlock\" knot is the way I tie my obi. I prefer it because it keeps the ends pointing down, can be cinched back up without having to start from scratch, and avoids an unsightly mess behind your back.

\\n

This video shows how to do it:

\\n'),\n", - " 'sustainable-living': (ArticleMetadata(title='Sustainable Living', author='Jim Shepich III', date=datetime.date(2025, 11, 20), lastmod=None, published=True, tags=['housekeeping', 'sustainability'], thumbnail=None),\n", - " '

As a homeowner, I have made it an ongoing goal to adopt sustainable and eco-friendly practices. As with all new undertakings, start with the small things and work your way up.

\\n

Before we get started, I do have to add one caveat. Disposable goods do have some legitimate use-cases, like sanitization and keeping things sterile (e.g. disposable needles, disinfectant wipes, toilet paper, etc).

\\n

In roughly increasing order of how difficult it was for me to build these habits, here are some of the things I do.

\\n

Cultivate a distaste for disposable products, especially single-use plastics

\\n

Buy a thing of Milano cookies and see that the cookies are separated with plastic risers into tiers, and how each tier is wrapped in plastic. The damn bag is already sealed!

\\n

Buy a pack of paper towel rolls, and notice that each roll is individually wrapped in plastic, inside the overall plastic-wrapped pack.

\\n

Just think about how pointless and wasteful all this single-use plastic is. Imagine a turtle swallowing a piece of this plastic and choking to death.

\\n

Now think about how much money you spend on paper towel. Think about how much money you spend on bottles of cleaning spray, and how much waste they generate.

\\n

This is your first step.

\\n

When you opt for reusable things instead of disposable things, it\\'s an investment that is both eco-friendly and financially rewarding.

\\n

You can\\'t always control how things are packaged, but if you develop this anti-disposable mindset, it will nudge you towards choosing better products.

\\n

Use natural lighting during the day

\\n

If you have LEDs, this won\\'t save much energy, but every little bit counts. And sunlight is good for the soul.

\\n

Buy in bulk

\\n

As containers grow in size, the surface-area-to-volume ratio typically decreases. In other words, you\\'ll generate much less waste buying a gallon jug of dishsoap than you would buying 4x 32oz bottles.

\\n

Typically, unit prices also go down when you buy larger containers or multi-packs, so it\\'s a win-win.

\\n

Use powder laundry detergent

\\n

Powder detergent is much cheaper than liquid detergent, and you need much less of it to wash your clothes. When I moved here a hear and a half ago, I bought a 16.5lb drum of Arm & Hammer powder detergent from Home Depot for a little over $30. I\\'m not even halfway through it.

\\n

Liquid detergents also sometimes contain byproducts of petroleum cracking.

\\n

Mix your own cleaning sprays

\\n

Blueland sells tablets that you can dissolve in water to make your own cleaning spray. They also sell volumetric reusable spray bottles, which you can use to easily ensure your cleaning solution is made to the correct concentration.

\\n

As of the time of writing, the tablets are around $2 unit price. Most cleaning sprays in disposable bottles cost at least twice as much and generate a bunch of plastic waste. Blueland uses compostable packaging.

\\n

Don\\'t use garbageware

\\n

Stop using garbageware (disposable utensils, plates, cups, etc). If you have a dishwasher, just use it. If you are hosting company and are overwhelmed by dishes, conscript someone to help you; most people are eager to be good guests. It\\'s just not worth generating so much waste. If you\\'re afraid of people breaking your fine china, get reusable plastic dishes. If someone serves you with garbageware, act offended. Cultural attitudes need to change in order for us to make progress.

\\n

If you get a paper cup at a restaurant, try to go without a straw or lid. If you are getting carryout, ask for no plasticware (or better yet, just don\\'t get carryout).

\\n

Use cloth towels instead of paper towels

\\n

Cleaning up after my son as he learns how to feed himself, I would go through a roll of paper towel in less than a week. So wasteful and so expensive. So, I bought these Mioeco-brand \"reusable paper towels\". People said that they\\'re a scam and just to use regular rags, but I did not have rags, and these seemed like they would have a good texture (they do).

\\n

I use these to wipe up his crumbs. When one gets too crumby, I rinse it off in the sink, wring it out, and use it again. Once I feel like it\\'s too gross, I throw it in a bucket I keep on my countertop. When I run low, I wash them.

\\n

Likewise, I use cloth napkins instead of paper napkins or paper towels. I got separate cloth napkins because the \"reusable paper towels\" have a nice rough texture for wiping hard surfaces, not skin.

\\n

Use a water flosser

\\n

When I first started trying to reduce single-use plastics, I switched from disposable floss picks to a reusable floss pick, which uses spools from floss cartridges. This is much less wasteful, but still generates plastic waste in the form of the empty cartridges, as well as the floss itself.

\\n

After I got some floss stuck in my teeth and had to go to the dentist to have it removed, I switched to a water flosser (we decided on the classic Waterpik). I now generate no plastic waste, and my teeth feel cleaner than ever. There\\'s no chance of floss getting stuck between my teeth now, and the water flosser minimizes gum bleeding too.

\\n

Turn off fans when not in use

\\n

Fans use a lot of energy (typically hundreds of watts; cf LED overhead lights, which typically use less than 10W). It\\'s important to use them to keep the air fresh and even out the temperature in the house, but be sure to turn them off when not in use.

\\n

Use canvas bags, boxes, or baskets instead of plastic grocery bags

\\n

I purchased a set of 4 canvas-and-mesh bags for groceries (including one with thermal lining) from Meijer several years ago. The bags have poles in them that span the top of a grocery cart, so they dangle down into it. Whenever I empty them, I try to toss them into the back of my car. If they get gross, I just toss them in the wash.

\\n

I also keep a few collapsible baskets in case I forget the bags, or in case the basket would be more convenient.

\\n

In one of the grocery bags, I keep smaller cloth bags for fresh fruits and vegetables. Whenever I use these, I wash them along with with towels, work gloves, bibs, etc.

\\n

If you slip up and forget your reusable grocery recepticles, just use their paper bags because you can compost them.

\\n

Use LED lighting

\\n

This takes more monetary investment that many of the other practices, but you also it\\'s also pretty much a one-and-done. LEDs use around 10x less power than incandescent lights, and they don\\'t dump a bunch of heat into their surroundings (which you have to air condition away in the summer), and they typically last much longer. I think I\\'ve only had to replace two LED bulbs in the last 2 years (and they were in the same fixture, so maybe there\\'s a problem with that).

\\n

Distill your own water

\\n

This doesn\\'t apply to everyone, just people who use a lot of distilled water. We use distilled water in our humidifiers, bottle washer, warm water dispenser (for making bottles), steam cleaner, Waterpik, etc. Some things don\\'t explicitly require distilled, but if you do use distilled, you don\\'t have to worry about having to clean out salt buildup.

\\n

We got a countertop still, and we typically have to run it 1 to 3 times per day. This keeps us from wasting around 20 plastic jugs of distilled water per day. The downside is that it does take a considerable amount of energy to distill the water, but it\\'s worth not needing to use all that plastic (or carry that many jugs in from the grocery store every week).

\\n

Go to the farmers\\' market

\\n

Fresh produce at the farmers\\' market is typically cheaper, tastier, less likely to be affected by recalls, and supports people in your community instead of supermarkets. Things at the farmers\\' market are also less likely to use single-use plastics.

\\n

Shop online less

\\n

There are some easy ways to make shopping online less wasteful. Whenever possible, ship in the manufacturer\\'s packaging, and choose lower-carbon delivery options.

\\n

But actually reducing how much you shop online is tough but rewarding. Shipping costs money and energy (even if the cost is incorporated into the price of the product instead of a distinct \"shipping fee\"), and inevitably requires more disposable packaging than what you would buy from a store.

\\n

I understand that it isn\\'t always possible to buy stuff locally, and if you are very busy, it can be hard to get to the store. But I\\'m not saying be perfect; just try.

\\n

Compost

\\n

I\\'m still in the process of getting started with composting.

\\n

For years, I have felt deeply bad about putting vegetable waste in plastic trash bags and shipping them off to the dump. So, as much as possible, I try to chuck food waste into the woods, just so the nutrients can return to the ecosystem. I purchased a countertop compost bin, in which I put fruit/vegetable waste. Every few days, I dump the bin onto a pile in the woods.

\\n

Soon, I\\'m planning to use a compost tumbler. I\\'ll also be able to put shredded office paper and packaging cardboard waste into the composter and turn it plus the biomass into fertile soil for gardening.

'),\n", - " 'stocking-up': (ArticleMetadata(title='Stocking Up', author='Jim Shepich III', date=datetime.date(2025, 11, 19), lastmod=None, published=True, tags=['housekeeping'], thumbnail=None),\n", - " \"

This is a strategy for managing household consumables that prioritizes never running out.

\\n

Think of some consumable groceries that you (1) use on a recurring basis, and (2) are either nonperishable or take significantly longer to expire than it takes for you to consume them. A few examples in my household:

\\n\\n

Now, we'll divide this set into two classes based on how fast you consume the smallest standalone unit: slow-burn consumables are those which take you a significantly longer interval to use (from start to finish) than the interval between trips to the grocery store where you get them; fast-burn consumables are those which you completely use on an interval that is smaller or similarly-sized than the interval between shopping trips.

\\n

For slow-burn consumables, keep one un-opened extra on hand. When you finish the opened unit, leave the empty package by your door to remind you to get another the next time you're out. Some examples of slow-burn consumables for me include:

\\n\\n

For fast-burn consumables, stash enough of them to last you at least one full between-shopping-trips interval around your house. Some examples of fast-burn consumables for me include:

\\n\\n

Multi-packs of fast-burn consumables can be treated en-bloc as slow-burn consumables, but only if you keep an entire extra multi-pack as backup. Some examples include:

\\n\\n

The main reason against storing a full backup of a multi-pack is storage.

\\n

Another principle of the stock-up strategy is that larger packs typically have lower unit prices, so when possible, opt for them.

\\n

The last thing to note is that when consumables you use regularly go on sale, it is an opportunity for you to stock up on as many as you have space for (and if perishable, can use before the expiration date).

\"),\n", - " 'set-up-the-toys': (ArticleMetadata(title='Set Up the Toys', author='Jim Shepich III', date=datetime.date(2026, 1, 14), lastmod=datetime.date(2026, 1, 14), published=True, tags=['parenting'], thumbnail=None),\n", - " '

Around the time our son started walking, my wife began the tradition of straightening up his toys every night so they would be ready for him to jumble up in the morning. When she became too pregnant with our daughter to continue, I assumed the mantle.

\\n

Ultimately, setting up the toys is something I do for my kids because I love them and feel like they deserve it. That\\'s what keeps me doing it night after night. On top of that, my son tends to be entertained by his toys (instead of my glasses, laptops, etc.) for much longer when they are set up in the morning.

\\n

Setting up the toys can also give you a sense of which toys your kid likes and how they interact with them, which in turn, can help you understand where your kid is developmentally. Occasionally, I try to build challenges into the setup, like hiding a favorite toy or a dry snack, or putting something out of reach to encourage them to use tools.

\\n

Examples

\\n

When I\\'m especially proud of my work (or when I just want to try to capture a slice of life), I take a picture of the toy setup. Here are some of the best.

\\n

March 2025

\\n

\"March

\\n

Bunky\\'s First Birthday

\\n

\"Bunky\\'s

\\n

Real-life Dancing Fruits for a \"berry sweet\" boy\\'s first birthday.

\\n

Baby\\'s First Dungeon

\\n

\"Baby\\'s

\\n

With dragons guarding his favorite toys (Pinks the bear, the \"Stink Flowers\", and his candied apple Pusheen), and Knight Owl waiting by the entrance with a sword and shield for the brave adventurer.

\\n

Big Brother\\'s Dungeon

\\n

\"Big

\\n

A dungeon that leads to his playpen, full of his best toys. I built this to make him feel special while my wife and I were in the labor and delivery ward for the scheduled delivery of our daughter.

\\n

October 2025

\\n

\"October

\\n

I started building a citadel out of his couch.

\\n

Attack on Junebug

\\n

\"Attack

\\n

I tried to encourage my son to play with my daughter by locking her stuff in his playpen and building ramps into it out of his couch.

\\n

Elfpocalypse

\\n

\"Elfpocalypse\"

\\n

In the thrilling conclusion to our first season of Elf-on-the-Shelf, the Elf leads the toys on a Christmas Day siege.

'),\n", - " 'do-what-you-love': (ArticleMetadata(title='Do What You Love', author='Jim Shepich III', date=datetime.date(2025, 6, 10), lastmod=None, published=True, tags=['career', 'ikigai', 'quote'], thumbnail=None),\n", - " '
\\n

Find a job you enjoy, and you will never have to work a day in your life.

\\n

— Mark Twain (allegedly)

\\n
\\n

When I was in middle school, I discovered in myself a passion for computer programming. But in high school, I decided that I did not want to pursue a career in computer science. I did not want my favorite pastime and form of creative expression to be commodified. I believed that if I were to let my programming be adulterated by the grimy reality of capital and wage slavery, I would love it less.

\\n

In college, I majored in chemistry, which I did and still do also love. But because chemistry was part of my compulsory education, I felt like my passion for it was already tainted by the academic industrial complex, so I had less to lose by making it my career. While I worked in a research lab, I found that the most fun I had was when I was setting up the automatic flash chromatograph, documenting how to use our analytical equipment, and programming a Python-based gas chromatogram analytical software.

\\n

At that point, I had an epiphany — by trying to keep programming separate from my career, I didn\\'t have many opportunities to do it anymore.

\\n

Nowadays, I have a MS in data science, and my day-to-day work mostly consists of devops. Although I spend most of my time working on projects that I don\\'t really care about (at least compared to my personal projects), I am happy that I made a career pivot that lets me program and work with computers. I can get excited about even the most topically insipid of projects if it is enough of a technical challenge. And I basically get to spend all day honing my skills, so that when I do have time to work on my hobby projects, I do them better.

\\n

I do not mean to diminish the tragedy of the fact that proletarians are only allowed to meaningfully pursue our passions insofar as we pervert said passions into part of the economic process. But the solution is not to rebel against this facet of industrial society by reserving your calling for nights, weekends, and holidays; you will be the only one who suffers in the end

\\n

So, back to the quote up top. When Mark Twain (or whoever) says \"never ... work a day in your life\", he does not mean that finding a job you like will liberate you from wage slavery. I think a more accurate (yet less quotable) way to look at it is this:

\\n
\\n

If you base your career on something you\\'re passionate about, then you\\'ll have an excuse to do something you love every day.

\\n

— Jim Shepich (actually)

\\n
'),\n", - " 'self-care-is-not-selfish': (ArticleMetadata(title='Self-Care is not Selfish', author='Jim Shepich III', date=datetime.date(2025, 5, 18), lastmod=None, published=True, tags=['adulting', 'health'], thumbnail=None),\n", - " '

To be a provider is to be a machine that converts time and energy into the resources your dependents need to survive. You may think that taking time to care for yourself is selfish. It is not. Pushing yourself to the point of physical or mental breakdown will only hurt your dependents in the long run. Self care is like sharpening a blade, cleaning a filter, or changing oil.

\\n

Develop the mindset that everything you do to take care of yourself is a short- and long-term investment in being better able to fulfill your responsibilities and provide for your loved ones.

\\n

As a husband and a father, I now have to take a much more intentional approach to my health and wellness. Here are some of the things I\\'ve found useful to keep me running like a well-oiled machine:

\\n\\n

Sources:\\n[1] https://doi.org/10.1523/JNEUROSCI.1171-18.2018 \\n[2] https://southpark.cc.com/video-clips/jgkzdr/south-park-beelzaboot\\n (alt: South Park S18E06: \"Freemium Isn\\'t Free\")

'),\n", - " 'blowouts': (ArticleMetadata(title='Blowouts', author='Jim Shepich III', date=datetime.date(2025, 11, 26), lastmod=None, published=True, tags=['parenting'], thumbnail=None),\n", - " \"

Here's my troubleshooting guide for if your infant is having a lot of blowouts:

\\n
    \\n
  1. If it's not that frequent (a couple times a week at the most), consider: shit happens. They'll blow out less when they transition to solids.
  2. \\n
  3. If they are blowing out through the leg, ensure the leg ruffles are fluffed out. You may need to ensure the waistband is not too high because that can sometimes create gaps along the legs.
  4. \\n
  5. If ② doesn't help, you may need to use the next size of diaper.
  6. \\n
  7. If they are blowing out up their backside, try to ensure the back of the diaper is as high up their backside as possible putting it on.
  8. \\n
  9. If ④ doesn't help, consider using a different brand of diaper, as it may fit your kid's butt better.
  10. \\n
  11. If ⑤ doesn't help, consider double-bagging your baby, by putting a diaper of a much larger size on over their regular one.
  12. \\n
\"),\n", - " 'vitamins': (ArticleMetadata(title='Vitamins & Supplements', author='Jim Shepich III', date=datetime.date(2025, 5, 18), lastmod=None, published=True, tags=['health'], thumbnail=None),\n", - " '

After several years of experimenting, I\\'ve found a regimen of vitamins and other supplements that have helped me manage some of my chronic health problems. Here\\'s what I take and why I take it:

\\n

Allergies

\\n\\n

Digestive Health

\\n\\n

In addition to these supplements, diet and exercise have made a huge impact for me. I think that my IBS was largely a symptom of fatty liver disease, so reducing fatty/high-carb foods and doing daily aerobic exercise has essentially cured me.

\\n

Headaches

\\n\\n

I\\'ve noticed that if I have a headache, it\\'s most likely because I\\'ve been sleep deprived most nights. There\\'s no substitute for getting adequate sleep, but Mg + CoQ10 are a good second.

\\n

Immune Support

\\n\\n

Misc

\\n'),\n", - " 'gear-for-new-parents': (ArticleMetadata(title='Gear for New Parents', author='Jim Shepich III', date=datetime.date(2024, 7, 12), lastmod=None, published=True, tags=['parenting', 'babyprep'], thumbnail=None),\n", - " '

I\\'ve compiled a list of useful items to purchase in preparation for a baby. I\\'ve excluded a bunch of the obvious stuff (e.g. diapers).

\\n

Whole House / No Specific Location

\\n\\n

Nursery

\\n\\n

Parents\\' Room

\\n\\n

Kitchen

\\n\\n

On-the-Go

\\n')}" - ] - }, - "execution_count": 48, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d48110fc", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "datetime.datetime(2024, 7, 12, 0, 0)" - ] - }, - "execution_count": 38, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "datetime(metadata['date'].year, metadata['date'].month, metadata['date'].day)" + "sites = {k:SiteConfig(**v) for k,v in config['sites'].items()}" ] }, { @@ -477,91 +79,9 @@ " # print(rss_feed.rss())" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "b068c448", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "de229ef3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SiteConfig(base_url='http://localhost:8000', git_repo=None, build_cache='./build', assets=['./assets'], web_root='./dist', articles=['./pages/*.md'])" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sites['main']" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "b7c12428", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['./build/dogma-jimfinium/superlock.md',\n", - " './build/dogma-jimfinium/sustainable-living.md',\n", - " './build/dogma-jimfinium/stocking-up.md',\n", - " './build/dogma-jimfinium/set-up-the-toys.md',\n", - " './build/dogma-jimfinium/babies-love-trash.md',\n", - " './build/dogma-jimfinium/do-what-you-love.md',\n", - " './build/dogma-jimfinium/self-care-is-not-selfish.md',\n", - " './build/dogma-jimfinium/temptation.md',\n", - " './build/dogma-jimfinium/blowouts.md',\n", - " './build/dogma-jimfinium/assets',\n", - " './build/dogma-jimfinium/vitamins.md',\n", - " './build/dogma-jimfinium/gear-for-new-parents.md']" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "glob.glob('./build/dogma-jimfinium/*')" - ] - }, { "cell_type": "code", "execution_count": 15, - "id": "d5bcdfcb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'dogma-jimfinium'" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "os.path.dirname('dogma-jimfinium/assets')" - ] - }, - { - "cell_type": "code", - "execution_count": null, "id": "70408b85", "metadata": {}, "outputs": [], @@ -570,7 +90,7 @@ " '''Generates HTML files for all of a given site's Markdown articles\n", " by interpolating the contents and metadata into the HTML templates.'''\n", "\n", - " for filestem, (metadata, content) in index:\n", + " for filestem, (metadata, content) in index.items():\n", " article = format_html_template(\n", " 'templates/components/blog_article.html',\n", " content = content,\n", @@ -581,7 +101,7 @@ " page = format_html_template(\n", " 'templates/pages/default.html',\n", " content = article,\n", - " **PARTIALS\n", + " partials = templates.partials\n", " )\n", "\n", " with open(f'{site.web_root.rstrip('/')}/{filestem}.html', 'w') as f:\n", @@ -644,9 +164,13 @@ "source": [ "def build_site(site: SiteConfig):\n", "\n", + " # Initialize the build cache and web root, in case they do not exist.\n", + " os.makedirs(site.build_cache, exist_ok = True)\n", + " os.makedirs(site.web_root, exist_ok = True)\n", + "\n", " # If the site is built from a git repo, pull that repo into the build cache.\n", " if site.git_repo:\n", - " pull_git_repo(site.git_repo)\n", + " pull_git_repo(site.git_repo, site.build_cache)\n", "\n", " # Copy the sites assets into the web root.\n", " copy_assets(site)\n", @@ -662,16 +186,12 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 16, "id": "a28b95a6", "metadata": {}, "outputs": [], "source": [ - "with open('config.yaml', 'r') as config_file:\n", - " config = yaml.safe_load(config_file.read())\n", - "sites = {k:SiteConfig(**v) for k,v in config['sites'].items()} \n", - "\n", - "build_site(sites['main'])" + "build_site(sites['dogma_jimfinium'])" ] } ],