diff --git a/.gitignore b/.gitignore index 87a67e9..d9d0210 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ shepich resume.pdf **/.env tmp build -dist \ No newline at end of file +dist +**/__pycache__ \ No newline at end of file diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 590ae18..0000000 Binary files a/__pycache__/main.cpython-312.pyc and /dev/null differ diff --git a/config.yaml b/config.yaml index e69de29..2ed6b33 100644 --- a/config.yaml +++ b/config.yaml @@ -0,0 +1,30 @@ +author: Jim Shepich III +site_defaults: + base_url: http://localhost:8000 + web_root: ./dist + templates: + partials: ./templates/partials + components: ./templates/components + pages: ./templates/pages +sites: + main: + build_cache: ./build + assets: + - ./assets + articles: + - ./pages/*.md + resume: + git_repo: ssh://gitea/jim/resume.git + build_cache: ./build/resume + assets: + - '{build_cache}/shepich_resume.pdf' + dogma_jimfinium: + base_url: http://localhost:8080/dogma-jimfinium + git_repo: ssh://gitea/jim/dogma-jimfinium.git + build_cache: ./build/dogma-jimfinium + web_root: ./dist/dogma-jimfinium + assets: + - '{build_cache}/assets' + articles: + - '{build_cache}/*.md' + \ No newline at end of file diff --git a/main.py b/main.py index 23ffae5..3fca3d1 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,9 @@ import os import subprocess import markdown import yaml -from datetime import datetime +import pydantic +from typing import Optional +from datetime import datetime, date def filepath_or_string(s: str) -> str: '''Loads the contents of a string if it is a filepath, otherwise returns the string.''' @@ -13,15 +15,37 @@ def filepath_or_string(s: str) -> str: return s -def load_markdown(md: str) -> tuple[dict, str]: - '''Loads a Markdown file into a (metadata: dict, content: str) pair.''' +with open('config.yaml', 'r') as config_file: + config = yaml.safe_load(config_file.read()) + +class SiteConfig(pydantic.BaseModel): + base_url: Optional[str] = config['site_defaults'].get('base_url') + git_repo: Optional[str] = config['site_defaults'].get('git_repo') + build_cache: Optional[str] = config['site_defaults'].get('build_cache') + assets: Optional[list] = config['site_defaults'].get('assets') + web_root: Optional[str] = config['site_defaults'].get('web_root') + articles: Optional[list] = config['site_defaults'].get('articles') + +class ArticleMetadata(pydantic.BaseModel): + title: str + author: Optional[str] = config.get('author') + date: date + lastmod: Optional[date] + published: bool + tags: list + thumbnail: Optional[str] = None + + + +def load_markdown(md: str) -> tuple[ArticleMetadata|None, str]: + '''Loads a Markdown file into a (metadata: ArticleMetadata, content: str) pair.''' # Load the file contents if a filepath is specified, and strip document delimiters ('---'). md = filepath_or_string(md).strip().strip('---').strip() # If there is no `---` delimiter, then the article has no metadata. if '---' not in md.strip('---'): - return {}, md + return None, md # Split the metadata from the contents. [raw_metadata, raw_article] = md.split('---') @@ -32,7 +56,7 @@ def load_markdown(md: str) -> tuple[dict, str]: # Convert the contents to a HTML string. content = markdown.markdown(raw_article) - return metadata, content + return ArticleMetadata(**metadata), content def format_html_template(template: str, **kwargs) -> str: @@ -107,5 +131,42 @@ def format_blog_tags(tags: list[str], template = 'templates/components/blog_tag. format_html_template(template, tag_name = t) for t in tags ] + +def build_blog_archive( + index: dict[str, tuple[str, str]], + page_template = 'templates/pages/default.html', + li_template = 'templates/components/blog_archive_li.html', + **kwargs + ) -> str: + '''Converts an index, formatted as filestem: (metadata, contents) dict, + into an HTML page containing the list of articles, sorted from newest to oldest. + + Note: partials must be expanded into the kwargs, as they are needed to generate + the overall page. + ''' + + # Add each article as a list item to an unordered list. + archive_html_content = '' + + # Interpolate the article into the overall page template. + archive_html_page = format_html_template( + page_template, + content = archive_html_content, + **kwargs + ) + + return archive_html_page + if __name__ == '__main__': pass \ No newline at end of file diff --git a/pages/home.html b/pages/home.html deleted file mode 100644 index 8b05c10..0000000 --- a/pages/home.html +++ /dev/null @@ -1,8 +0,0 @@ -
-

Welcome!

-
-

Welcome to my little corner of the Internet! My name is Jim Shepich (@epicshepich). I like to introduce myself as a jack-of-all-trades with a Master's in data science. My main interests include STEM, martial arts (especially jūjutsu), sci-fi and fantasy, tabletop and video gaming, cartoons and comics, and DIY, but I also love to branch out and learn new things! My dream is to become "The Most Interesting Man in the World."

- -

This website started as a way for me to exercise and showcase my skills with vanilla HTML, JavaScript, CSS, and PHP. Now, it's just a matter of filling the site with content! My vision for this website is for it to serve as a gift to my future reincarnated self (if that's what happens) — a way to pass on the little tips and tricks, bits of wisdom, resources, and treasures I've accumulated over the course of my life (think new game plus). And if some other kindred spirits find something here that helps them on their journey, that's a bonus!

- -
diff --git a/pages/home.md b/pages/home.md new file mode 100644 index 0000000..588b968 --- /dev/null +++ b/pages/home.md @@ -0,0 +1,10 @@ +--- +title: Welcome +date: 2026-01-30 +published: true +tags: [] +--- + +Welcome to my little corner of the Internet! My name is Jim Shepich (@epicshepich). I like to introduce myself as a jack-of-all-trades with a Master's in data science. My main interests include STEM, martial arts (especially jūjutsu), sci-fi and fantasy, tabletop and video gaming, cartoons and comics, and DIY, but I also love to branch out and learn new things! My dream is to become "The Most Interesting Man in the World." + +This website started as a way for me to exercise and showcase my skills with vanilla HTML, JavaScript, CSS, and PHP. Now, it's just a matter of filling the site with content! My vision for this website is for it to serve as a gift to my future reincarnated self (if that's what happens) — a way to pass on the little tips and tricks, bits of wisdom, resources, and treasures I've accumulated over the course of my life (think _new game plus_). And if some other kindred spirits find something here that helps them on their journey, that's a bonus! diff --git a/requirements.txt b/requirements.txt index 735541d..f9c7c46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ ipykernel markdown -pyyaml \ No newline at end of file +pyyaml +rfeed \ No newline at end of file diff --git a/templates/blog_post.html b/templates/blog_post.html index 227fdfd..3a7c584 100644 --- a/templates/blog_post.html +++ b/templates/blog_post.html @@ -9,11 +9,11 @@
-

{metadata__title}

+

{metadata.title}

By
-
First published: -
Last modified: +
First published: +
Last modified:

{content}
diff --git a/templates/components/blog_archive_li.html b/templates/components/blog_archive_li.html index a06da53..4fe2927 100644 --- a/templates/components/blog_archive_li.html +++ b/templates/components/blog_archive_li.html @@ -1 +1 @@ -
  • {metadata__date} - {metadata__title} {blog_tags}
  • +
  • {metadata.date} - {metadata.title} {blog_tags}
  • diff --git a/templates/components/blog_article.html b/templates/components/blog_article.html index b8acdf0..452f7e9 100644 --- a/templates/components/blog_article.html +++ b/templates/components/blog_article.html @@ -1,11 +1,11 @@
    -

    {metadata__title}

    +

    {metadata.title}

    By
    -
    First published: -
    Last modified: +
    First published: +
    Last modified:

    {content} -
    +


    {blog_tags}

    \ No newline at end of file diff --git a/templates/pages/default.html b/templates/pages/default.html new file mode 100644 index 0000000..bcb8ed9 --- /dev/null +++ b/templates/pages/default.html @@ -0,0 +1,14 @@ + + + + + {partials__default_css} + {partials__header} + {partials__nav} + + +
    + {content} +
    + {partials__footer} + \ No newline at end of file diff --git a/testbench.ipynb b/testbench.ipynb index b36751b..959d6ea 100644 --- a/testbench.ipynb +++ b/testbench.ipynb @@ -27,15 +27,20 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "207d2510", "metadata": {}, "outputs": [], "source": [ "import os\n", + "import shutil\n", "import markdown\n", "import yaml\n", "import subprocess\n", + "import rfeed\n", + "import pydantic\n", + "from typing import Optional, Union, Literal, BinaryIO, Any\n", + "\n", "\n", "\n", "from datetime import datetime\n", @@ -44,24 +49,68 @@ }, { "cell_type": "code", - "execution_count": 12, - "id": "d2361c42", + "execution_count": null, + "id": "4b17a3ed", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'dist/assets'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "metadata, content = load_markdown('tmp/dogma-jimfinium/blowouts.md')\n", - "content = content.replace('src=\"assets', 'src=\"../tmp/dogma-jimfinium/assets')" + "PARTIALS = load_partials()\n", + "shutil.rmtree('dist/assets', ignore_errors=True)\n", + "shutil.copytree('assets','dist/assets')" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, + "id": "d2361c42", + "metadata": {}, + "outputs": [ + { + "ename": "ValidationError", + "evalue": "2 validation errors for ArticleMetadata\ndate\n Input should be a valid string [type=string_type, input_value=datetime.date(2026, 1, 30), input_type=date]\n For further information visit https://errors.pydantic.dev/2.12/v/string_type\nlastmod\n Field required [type=missing, input_value={'title': 'Welcome', 'dat...shed': True, 'tags': []}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValidationError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m metadata, content = \u001b[43mload_markdown\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mpages/home.md\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 2\u001b[39m \u001b[38;5;66;03m# content = content.replace('src=\"assets', 'src=\"../tmp/dogma-jimfinium/assets')\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/projects/shepich.com/main.py:59\u001b[39m, in \u001b[36mload_markdown\u001b[39m\u001b[34m(md)\u001b[39m\n\u001b[32m 56\u001b[39m \u001b[38;5;66;03m# Convert the contents to a HTML string.\u001b[39;00m\n\u001b[32m 57\u001b[39m content = markdown.markdown(raw_article)\n\u001b[32m---> \u001b[39m\u001b[32m59\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mArticleMetadata\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mmetadata\u001b[49m\u001b[43m)\u001b[49m, content\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/projects/shepich.com/.venv/lib/python3.12/site-packages/pydantic/main.py:250\u001b[39m, in \u001b[36mBaseModel.__init__\u001b[39m\u001b[34m(self, **data)\u001b[39m\n\u001b[32m 248\u001b[39m \u001b[38;5;66;03m# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks\u001b[39;00m\n\u001b[32m 249\u001b[39m __tracebackhide__ = \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m250\u001b[39m validated_self = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m__pydantic_validator__\u001b[49m\u001b[43m.\u001b[49m\u001b[43mvalidate_python\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mself_instance\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 251\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m validated_self:\n\u001b[32m 252\u001b[39m warnings.warn(\n\u001b[32m 253\u001b[39m \u001b[33m'\u001b[39m\u001b[33mA custom validator is returning a value other than `self`.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m'\u001b[39m\n\u001b[32m 254\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mReturning anything other than `self` from a top level model validator isn\u001b[39m\u001b[33m'\u001b[39m\u001b[33mt supported when validating via `__init__`.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 255\u001b[39m \u001b[33m'\u001b[39m\u001b[33mSee the `model_validator` docs (https://docs.pydantic.dev/latest/concepts/validators/#model-validators) for more details.\u001b[39m\u001b[33m'\u001b[39m,\n\u001b[32m 256\u001b[39m stacklevel=\u001b[32m2\u001b[39m,\n\u001b[32m 257\u001b[39m )\n", + "\u001b[31mValidationError\u001b[39m: 2 validation errors for ArticleMetadata\ndate\n Input should be a valid string [type=string_type, input_value=datetime.date(2026, 1, 30), input_type=date]\n For further information visit https://errors.pydantic.dev/2.12/v/string_type\nlastmod\n Field required [type=missing, input_value={'title': 'Welcome', 'dat...shed': True, 'tags': []}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing" + ] + } + ], + "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": "0bb15524", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, "id": "ed7b3b2f", "metadata": {}, "outputs": [], "source": [ - "PARTIALS = load_partials()\n", - "html = format_html_template('templates/blog_post.html', content = content, **{'metadata__'+k:v for k,v in metadata.items()}, **PARTIALS)\n", + "html = format_html_template('templates/pages/default.html', content = content, **{'metadata.'+k:v for k,v in metadata.items()}, **PARTIALS)\n", "with open('dist/home.html', 'w') as f:\n", " f.write(html)" ] @@ -102,13 +151,14 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": null, "id": "944a5efd", "metadata": {}, "outputs": [], "source": [ - "run(f'rm -rf dist/dogma-jimfinium && mkdir -p dist/dogma-jimfinium')\n", - "run(f'cp -r build/dogma-jimfinium/assets dist/dogma-jimfinium')\n", + "shutil.rmtree('dist/dogma-jimfinium', ignore_errors=True)\n", + "os.makedirs('dist/dogma-jimfinium', exist_ok=True)\n", + "shutil.copytree('build/dogma-jimfinium/assets', 'dist/dogma-jimfinium/assets')\n", "\n", "index = {}\n", "\n", @@ -129,26 +179,122 @@ " 'templates/components/blog_article.html',\n", " content = content,\n", " blog_tags = ' '.join(format_blog_tags(metadata['tags'])),\n", - " **{'metadata__'+k:v for k,v in metadata.items()}\n", + " **{'metadata.'+k:v for k,v in metadata.items()}\n", " )\n", - " html = format_html_template('templates/simple.html', content = article_html, **PARTIALS)\n", + " html = format_html_template('templates/pages/default.html', content = article_html, **PARTIALS)\n", " \n", " # Write the HTML file to /dist/dogma-jimfinium.\n", " with open(f'dist/dogma-jimfinium/{article_filestem}.html', 'w') as f:\n", " f.write(html)\n", "\n", "\n", - "index_html = build_blog_archive(index, metadata__title = 'Dogma Jimfinium | Index')\n", + "index_html = build_blog_archive(index, metadata.title = 'Dogma Jimfinium | Index', **PARTIALS)\n", "# Write the HTML file to /dist/dogma-jimfinium.\n", "with open(f'dist/dogma-jimfinium/index.html', 'w') as f:\n", " f.write(index_html)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "132a32ec", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime.date(2024, 7, 12)" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "metadata['date']" + ] + }, + { + "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)" + ] + }, { "cell_type": "code", "execution_count": null, "id": "e32458c7", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Dogma Jimfiniumhttp://localhost:8000/dogma-jimfinium/rssDogma Jimfiniumen-USThu, 29 Jan 2026 16:29:57 GMTrfeed v1.1.1https://github.com/svpino/rfeed/blob/master/README.mdSuperlockhttp://localhost:8000/dogma-jimfinium/superlockJim Shepich IIIWed, 26 Nov 2025 00:00:00 GMTsuperlockSustainable Livinghttp://localhost:8000/dogma-jimfinium/sustainable-livingJim Shepich IIIThu, 20 Nov 2025 00:00:00 GMTsustainable-livingStocking Uphttp://localhost:8000/dogma-jimfinium/stocking-upJim Shepich IIIWed, 19 Nov 2025 00:00:00 GMTstocking-upSet Up the Toyshttp://localhost:8000/dogma-jimfinium/set-up-the-toysJim Shepich IIIWed, 14 Jan 2026 00:00:00 GMTset-up-the-toysDo What You Lovehttp://localhost:8000/dogma-jimfinium/do-what-you-loveJim Shepich IIITue, 10 Jun 2025 00:00:00 GMTdo-what-you-loveSelf-Care is not Selfishhttp://localhost:8000/dogma-jimfinium/self-care-is-not-selfishJim Shepich IIISun, 18 May 2025 00:00:00 GMTself-care-is-not-selfishBlowoutshttp://localhost:8000/dogma-jimfinium/blowoutsJim Shepich IIIWed, 26 Nov 2025 00:00:00 GMTblowoutsVitamins & Supplementshttp://localhost:8000/dogma-jimfinium/vitaminsJim Shepich IIISun, 18 May 2025 00:00:00 GMTvitaminsGear for New Parentshttp://localhost:8000/dogma-jimfinium/gear-for-new-parentsJim Shepich IIIFri, 12 Jul 2024 00:00:00 GMTgear-for-new-parents\n" + ] + } + ], + "source": [ + "rss_feed = rfeed.Feed(\n", + " title = \"Dogma Jimfinium\",\n", + " link = \"http://localhost:8000/dogma-jimfinium/rss\",\n", + " description = \"Dogma Jimfinium\",\n", + " language = \"en-US\",\n", + " lastBuildDate = datetime.now(),\n", + " items = [\n", + " rfeed.Item(\n", + " title = metadata['title'],\n", + " link = f\"http://localhost:8000/dogma-jimfinium/{filestem}\", \n", + " description = metadata.get('description'),\n", + " author = metadata.get('author', 'Jim Shepich III'),\n", + " guid = rfeed.Guid(filestem),\n", + " pubDate = datetime(metadata['date'].year, metadata['date'].month, metadata['date'].day)\n", + " )\n", + " for filestem, (metadata, _) in index.items()\n", + " ]\n", + ")\n", + "\n", + "print(rss_feed.rss())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c160693", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57ef8185", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b068c448", + "metadata": {}, "outputs": [], "source": [] }, @@ -159,31 +305,8 @@ "metadata": {}, "outputs": [], "source": [ - "def build_blog_archive(index: dict[str, tuple[str, str]], template = 'templates/simple.html', **kwargs) -> str:\n", - " '''Converts an index, formatted as filestem: (metadata, contents) dict,\n", - " into an HTML page containing the list of articles, sorted from newest to oldest.'''\n", - "\n", - " # Add each article in the format `YYYY-MM-DD - Title`\n", - " index_html_content = '
      '\n", - " for article, (metadata, contents) in sorted(index.items(), key = lambda item: item[1][0]['date'])[::-1]:\n", - " blog_tags = ' '.join(format_blog_tags(metadata['tags']))\n", - " index_html_content += format_html_template(\n", - " 'templates/components/blog_archive_li.html',\n", - " article_filestem = article,\n", - " blog_tags = blog_tags,\n", - " **{'metadata__'+k:v for k,v in metadata.items()}\n", - " \n", - " )\n", - " index_html_content +='
    '\n", - "\n", - " index_html_page = format_html_template(\n", - " template, \n", - " content = index_html_content, \n", - " **kwargs,\n", - " **PARTIALS\n", - " )\n", - " \n", - " return index_html_page" + "def build_site(site_config: SiteConfig):\n", + " " ] } ],