refactor #1
9
.gitignore
vendored
9
.gitignore
vendored
@ -1 +1,8 @@
|
|||||||
shepich resume.pdf
|
shepich resume.pdf
|
||||||
|
**/.venv
|
||||||
|
**/.env
|
||||||
|
tmp
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
**/__pycache__
|
||||||
|
custom.yaml
|
||||||
21
agenda.md
21
agenda.md
@ -1,21 +0,0 @@
|
|||||||
## Pages
|
|
||||||
- Home
|
|
||||||
- About
|
|
||||||
- CV
|
|
||||||
- Everything in as much detail as possible
|
|
||||||
- Projects
|
|
||||||
- E&E
|
|
||||||
- M|Chroma
|
|
||||||
- SI-Formatter
|
|
||||||
- Socials (maybe better as a footer)
|
|
||||||
- Instagram
|
|
||||||
- Facebook
|
|
||||||
- LinkedIn
|
|
||||||
- GitHub
|
|
||||||
- MAL
|
|
||||||
- Bookmarks (?)
|
|
||||||
|
|
||||||
## Reference Material
|
|
||||||
- https://www.tomscott.com/
|
|
||||||
- http://vihart.com/
|
|
||||||
- https://www.singingbanana.com/
|
|
||||||
41
config.yaml
Normal file
41
config.yaml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
author: Jim Shepich III
|
||||||
|
templates_folder: ./site/templates
|
||||||
|
sites:
|
||||||
|
main:
|
||||||
|
title: Jimlab
|
||||||
|
base_url: http://localhost:8000
|
||||||
|
web_root: ./dist
|
||||||
|
build_cache: ./site
|
||||||
|
assets:
|
||||||
|
- /assets
|
||||||
|
articles:
|
||||||
|
- ./pages/*.md
|
||||||
|
template_selections:
|
||||||
|
article: templates.components.simple_article
|
||||||
|
|
||||||
|
resume:
|
||||||
|
title: Resume
|
||||||
|
base_url: http://localhost:8000
|
||||||
|
web_root: ./dist
|
||||||
|
git_repo:
|
||||||
|
url: ssh://gitea/jim/resume.git
|
||||||
|
build_cache: ./build/resume
|
||||||
|
assets:
|
||||||
|
- 'shepich_resume.pdf'
|
||||||
|
|
||||||
|
dogma_jimfinium:
|
||||||
|
title: Dogma Jimfinium
|
||||||
|
description: May it bolster the skills of all who read it.
|
||||||
|
base_url: http://localhost:8000/dogma-jimfinium
|
||||||
|
git_repo:
|
||||||
|
url: ssh://gitea/jim/dogma-jimfinium.git
|
||||||
|
branch: pub
|
||||||
|
build_cache: ./build/dogma-jimfinium
|
||||||
|
web_root: ./dist/dogma-jimfinium
|
||||||
|
assets:
|
||||||
|
- assets
|
||||||
|
articles:
|
||||||
|
- '*.md'
|
||||||
|
addons:
|
||||||
|
- rss
|
||||||
|
|
||||||
1137
data/lists.json
1137
data/lists.json
File diff suppressed because it is too large
Load Diff
@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"home" : {
|
|
||||||
"name" : "Home",
|
|
||||||
"query_value" : "home",
|
|
||||||
"file" : "home.html",
|
|
||||||
"index" : 0
|
|
||||||
},
|
|
||||||
|
|
||||||
"404" : {
|
|
||||||
"name" : "404",
|
|
||||||
"query_value" : "404",
|
|
||||||
"file" : "404.html",
|
|
||||||
"index" : -1
|
|
||||||
},
|
|
||||||
|
|
||||||
"about" : {
|
|
||||||
"name" : "About",
|
|
||||||
"query_value" : "about",
|
|
||||||
"file" : "about.html",
|
|
||||||
"index" : 1
|
|
||||||
},
|
|
||||||
|
|
||||||
"resume" : {
|
|
||||||
"name" : "Resume",
|
|
||||||
"query_value" : "resume",
|
|
||||||
"link" : "pages/shepich resume.pdf",
|
|
||||||
"index" : 2
|
|
||||||
},
|
|
||||||
|
|
||||||
"epics" : {
|
|
||||||
"name" : "Epics & Emprises",
|
|
||||||
"query_value" : "epics",
|
|
||||||
"link" : "https://epics.shepich.com",
|
|
||||||
"index" : -1
|
|
||||||
},
|
|
||||||
|
|
||||||
"lists" : {
|
|
||||||
"name" : "Lists",
|
|
||||||
"query_value" : "lists",
|
|
||||||
"file" : "lists.html",
|
|
||||||
"index" : -1
|
|
||||||
},
|
|
||||||
|
|
||||||
"don-info" : {
|
|
||||||
"name" : "Info for Don",
|
|
||||||
"query_value" : "don-info",
|
|
||||||
"file" : "don-info.html",
|
|
||||||
"index" : -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"platform": "Instagram",
|
|
||||||
"icon": "instagram",
|
|
||||||
"link": "https://instagram.com/epicshepich"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"platform": "GitHub",
|
|
||||||
"icon": "github",
|
|
||||||
"link": "https://github.com/epicshepich"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"platform": "Facebook",
|
|
||||||
"icon": "facebook-square",
|
|
||||||
"link": "https://www.facebook.com/jim.shepich/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"platform": "LinkedIn",
|
|
||||||
"icon": "linkedin",
|
|
||||||
"link": "https://www.linkedin.com/in/jshepich/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"platform": "Discord",
|
|
||||||
"icon": "discord",
|
|
||||||
"link": "https://discordapp.com/users/epicshepich#0131"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
47
index.php
47
index.php
@ -1,47 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<head>
|
|
||||||
<script src="scripts/vendor/jquery-3.6.0.min.js"></script>
|
|
||||||
<script src="https://kit.fontawesome.com/09beb7ae4a.js" crossorigin="anonymous"></script>
|
|
||||||
<link rel="stylesheet" type="text/css" href="styles/reset.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="styles/common.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="styles/layout.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="styles/theme.css">
|
|
||||||
|
|
||||||
<?php include 'scripts/query_handler.php';?>
|
|
||||||
<title><?php
|
|
||||||
echo $page->name." | Jim Shepich";
|
|
||||||
?></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header id="main-header" class="no-highlight">
|
|
||||||
<span class="silver-text">Jim Shepich III</span>
|
|
||||||
</header>
|
|
||||||
<nav id="main-navbar" class="no-highlight">
|
|
||||||
<?php include 'scripts/nav.php';?>
|
|
||||||
</nav>
|
|
||||||
<main>
|
|
||||||
<?php
|
|
||||||
if(isset($page->link)){
|
|
||||||
echo "<script>window.location.href = '".$page->link."'</script>";
|
|
||||||
//If the directory assigns the page to an external link, redirect to that location.
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($page->iframe)){
|
|
||||||
echo "<iframe id='content' src='pages/".$page->file."'></iframe>";
|
|
||||||
//If the directory says to embed the page in an iframe, then do that.
|
|
||||||
} else {
|
|
||||||
echo file_get_contents(__DIR__ ."/pages/".$page->file);
|
|
||||||
//Otherwise, just write the HTML of the page in the content area.
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<div id="socials" class="silver-text"><?php include 'scripts/footer.php';?></div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
<script src="scripts/resize.js"></script>
|
|
||||||
</html>
|
|
||||||
78
jimsite/__init__.py
Normal file
78
jimsite/__init__.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
from dotmap import DotMap
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def build_site(site: SiteConfig, templates: DotMap):
|
||||||
|
'''The pipeline for building a site defined in config.yaml.'''
|
||||||
|
|
||||||
|
logger.info(f'Building site "{site.title}".')
|
||||||
|
|
||||||
|
# Do not build a site marked as unpublished.
|
||||||
|
if not site.published:
|
||||||
|
logger.info(f'"{site.title}" is not published. Skipping.')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Initialize the build cache and web root, in case they do not exist.
|
||||||
|
logger.debug(f'Creating build cache: {site.build_cache}')
|
||||||
|
os.makedirs(site.build_cache, exist_ok = True)
|
||||||
|
logger.debug(f'Creating web root: {site.web_root}')
|
||||||
|
os.makedirs(site.web_root, exist_ok = True)
|
||||||
|
|
||||||
|
# If the site is built from a git repo, pull that repo into the build cache.
|
||||||
|
if site.git_repo:
|
||||||
|
logger.info(f'Cloning/pulling git repo from {site.git_repo}')
|
||||||
|
pull_git_repo(site.git_repo, site.build_cache)
|
||||||
|
|
||||||
|
# Copy the sites assets into the web root.
|
||||||
|
logger.info(f'Copying static assets.')
|
||||||
|
copy_assets(site)
|
||||||
|
|
||||||
|
# Load the site's articles into an index.
|
||||||
|
logger.info('Building index of articles.')
|
||||||
|
index = build_index(site)
|
||||||
|
|
||||||
|
# Determine which templates are to be used for explicit applications, e.g.
|
||||||
|
# the tag component.
|
||||||
|
logger.info('Loading selected templates.')
|
||||||
|
template_selections = TemplateSelections(site, templates)
|
||||||
|
|
||||||
|
# Generate HTML pages for the articles.
|
||||||
|
logger.info('Building articles.')
|
||||||
|
build_articles(site, index, templates, template_selections)
|
||||||
|
|
||||||
|
if len(site.articles or []):
|
||||||
|
logger.info('Building archive of articles.')
|
||||||
|
build_blog_archive(site, index, template_selections, templates = templates)
|
||||||
|
|
||||||
|
if 'rss' in (site.addons or []):
|
||||||
|
logger.info('Building RSS feed.')
|
||||||
|
build_rss_feed(site, index)
|
||||||
|
else:
|
||||||
|
logger.debug('Addon "rss" not elected.')
|
||||||
|
|
||||||
|
|
||||||
|
def build(config_filepath = './config.yaml'):
|
||||||
|
'''Pipeline for building the entire website, including all sites defined in config.yaml.'''
|
||||||
|
logger.info('Loading config.')
|
||||||
|
with open(config_filepath, 'r') as config_file:
|
||||||
|
config = yaml.safe_load(config_file.read())
|
||||||
|
|
||||||
|
logger.info('Loading global templates.')
|
||||||
|
templates = map_templates(config['templates_folder'])
|
||||||
|
|
||||||
|
for site in config['sites'].values():
|
||||||
|
build_site(SiteConfig(**site), templates)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
15
jimsite/__main__.py
Normal file
15
jimsite/__main__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import argparse
|
||||||
|
from jimsite import build
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='Jimsite',
|
||||||
|
description='A Python-based templating engine for building static websites from Markdown documents.',
|
||||||
|
epilog='Gonna be a hot one!'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('-c', '--config', type=str, default='config.yaml', help='Specifies the path to a YAML config file; defaults to config.yaml in the CWD.')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
build(config_filepath = args.config)
|
||||||
132
jimsite/articles.py
Normal file
132
jimsite/articles.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import yaml
|
||||||
|
import markdown
|
||||||
|
import pydantic
|
||||||
|
from typing import Optional, Union
|
||||||
|
from dotmap import DotMap
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from .common import filepath_or_string, SiteConfig
|
||||||
|
from .templating import format_html_template, TemplateSelections
|
||||||
|
|
||||||
|
class ArticleMetadata(pydantic.BaseModel):
|
||||||
|
'''A model for the YAML frontmatter included with Markdown articles.'''
|
||||||
|
title: str
|
||||||
|
date: date
|
||||||
|
published: bool
|
||||||
|
tags: list
|
||||||
|
author: Optional[str] = None
|
||||||
|
lastmod: Optional[date] = None
|
||||||
|
thumbnail: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
class Article(pydantic.BaseModel):
|
||||||
|
'''A model for a Markdown article, including its YAML frontmatter
|
||||||
|
metadata, as well as its path relative to the build cache root.'''
|
||||||
|
path: str
|
||||||
|
content: str
|
||||||
|
metadata: Optional[ArticleMetadata] = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_markdown(md: str) -> tuple[Optional[ArticleMetadata], 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 None, md
|
||||||
|
|
||||||
|
# Split the metadata from the contents.
|
||||||
|
[raw_metadata, raw_article] = md.split('---')
|
||||||
|
|
||||||
|
# Use YAML to parse the metadata.
|
||||||
|
metadata = yaml.safe_load(raw_metadata)
|
||||||
|
|
||||||
|
# Convert the contents to a HTML string.
|
||||||
|
content = markdown.markdown(raw_article)
|
||||||
|
|
||||||
|
return ArticleMetadata(**metadata), content
|
||||||
|
|
||||||
|
|
||||||
|
def build_index(site: SiteConfig) -> dict[str, Article]:
|
||||||
|
'''Loads the sites articles into an index mapping the filename
|
||||||
|
to an Article object.'''
|
||||||
|
|
||||||
|
index = {}
|
||||||
|
|
||||||
|
# Expand any globbed expressions.
|
||||||
|
expanded_article_list = []
|
||||||
|
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.removeprefix('./').lstrip('/')}")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
for article_full_path in expanded_article_list:
|
||||||
|
metadata, content = load_markdown(article_full_path)
|
||||||
|
|
||||||
|
# Skip unpublished articles.
|
||||||
|
if not metadata.published:
|
||||||
|
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')
|
||||||
|
|
||||||
|
index[article_path] = Article(
|
||||||
|
path = article_path,
|
||||||
|
content = content,
|
||||||
|
metadata = metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
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(tag_template, tag_name = t, **kwargs) for t in tags
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_articles(
|
||||||
|
site: SiteConfig,
|
||||||
|
index: dict[str, Article],
|
||||||
|
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 article in index.values():
|
||||||
|
article_html = format_html_template(
|
||||||
|
template_selections['article'],
|
||||||
|
article = article,
|
||||||
|
blog_tags = ' '.join(format_article_tags(
|
||||||
|
article.metadata.tags, template_selections['tag'], site = site
|
||||||
|
)),
|
||||||
|
templates = templates,
|
||||||
|
site = site
|
||||||
|
)
|
||||||
|
|
||||||
|
page_html = format_html_template(
|
||||||
|
template_selections['page'],
|
||||||
|
content = article_html,
|
||||||
|
templates = templates,
|
||||||
|
site = site
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(f"{site.web_root.rstrip('/')}/{article.path}", 'w') as f:
|
||||||
|
f.write(page_html)
|
||||||
|
|
||||||
|
|
||||||
54
jimsite/assets.py
Normal file
54
jimsite/assets.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import shutil
|
||||||
|
from .common import run, GitRepo, SiteConfig
|
||||||
|
|
||||||
|
|
||||||
|
def pull_git_repo(repo: GitRepo, build_cache: str) -> None:
|
||||||
|
'''Pulls/clones a repo into the build cache directory.'''
|
||||||
|
|
||||||
|
# If a repo exists in the build cache, pull to it.
|
||||||
|
if os.path.exists(f'{build_cache}/.git'):
|
||||||
|
|
||||||
|
# If a branch is specified, check out to it.
|
||||||
|
if repo.branch is not None:
|
||||||
|
run(f"git checkout {repo.branch}")
|
||||||
|
run(f"git -C {build_cache} pull {repo.remote}{' '+repo.branch if repo.branch else ''}")
|
||||||
|
|
||||||
|
# If the build cache is empty, clone the repo into it.
|
||||||
|
else:
|
||||||
|
run(f"git clone {repo.url}{(' -b '+repo.branch) if repo.branch else ''} -o {repo.remote} {build_cache}")
|
||||||
|
|
||||||
|
|
||||||
|
def copy_assets(site: SiteConfig) -> None:
|
||||||
|
'''Copies the list of site assets from the build cache to the web root.
|
||||||
|
The asset list can include globs (*). All paths are resolved relative to
|
||||||
|
the build cache root, and will be copied to an analogous location in the
|
||||||
|
web root.'''
|
||||||
|
|
||||||
|
# Expand any globbed expressions.
|
||||||
|
expanded_asset_list = []
|
||||||
|
for a in site.assets:
|
||||||
|
expanded_asset_list.extend(
|
||||||
|
# Assets are defined relative to the build cache; construct the full path.
|
||||||
|
glob.glob(f'{site.build_cache}/{a.lstrip("/")}')
|
||||||
|
)
|
||||||
|
|
||||||
|
for asset in expanded_asset_list:
|
||||||
|
|
||||||
|
# Construct the destination path analogous to the source path
|
||||||
|
# but in the web root instead of the build cache.
|
||||||
|
destination = f'{site.web_root}/What the program does{a.lstrip("/")}'
|
||||||
|
|
||||||
|
# Delete existing files.
|
||||||
|
shutil.rmtree(destination, ignore_errors=True)
|
||||||
|
|
||||||
|
# Copy the asset.
|
||||||
|
if os.path.isdir(asset):
|
||||||
|
shutil.copytree(asset, destination)
|
||||||
|
elif os.path.isfile(asset):
|
||||||
|
shutil.copyfile(asset, destination)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return None
|
||||||
136
jimsite/blog.py
Normal file
136
jimsite/blog.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import rfeed
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
from .common import SiteConfig
|
||||||
|
from .articles import ArticleMetadata, Article, format_article_tags
|
||||||
|
from .templating import format_html_template, TemplateSelections
|
||||||
|
|
||||||
|
|
||||||
|
def build_tag_index(index: dict[str, Article], wildcard ='*') -> dict[str, list[Article]]:
|
||||||
|
'''Creates an inverted index mapping each article tag to a postings list of articles
|
||||||
|
with said tag.'''
|
||||||
|
tag_index = {}
|
||||||
|
|
||||||
|
# Index the articles in ascending order of original publication date.
|
||||||
|
for article in sorted(index.values(), key = lambda a: a.metadata.date):
|
||||||
|
|
||||||
|
# Add all articles to the wildcard tag's postings list.
|
||||||
|
if wildcard is not None:
|
||||||
|
tag_index[wildcard] = (tag_index.get(wildcard,[])) + [article]
|
||||||
|
|
||||||
|
# Add the article to each of its tags' posting lists.
|
||||||
|
for tag in article.metadata.tags:
|
||||||
|
tag_index[tag] = (tag_index.get(tag,[])) + [article]
|
||||||
|
|
||||||
|
return tag_index
|
||||||
|
|
||||||
|
|
||||||
|
def build_blog_archive(
|
||||||
|
site: SiteConfig,
|
||||||
|
index: dict[str, Article],
|
||||||
|
template_selections: TemplateSelections,
|
||||||
|
**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_list = '<ul>'
|
||||||
|
for article in sorted(index.values(), key = lambda a: a.metadata.date)[::-1]:
|
||||||
|
|
||||||
|
# Generate HTML for the article (including metadata tags).
|
||||||
|
archive_html_list += format_html_template(
|
||||||
|
template_selections['archive_li'],
|
||||||
|
article_filestem = article,
|
||||||
|
blog_tags = ' '.join(format_article_tags(article.metadata.tags, template_selections['tag'], site = site)),
|
||||||
|
article = article,
|
||||||
|
site = site,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
archive_html_list +='</ul>'
|
||||||
|
|
||||||
|
tag_index = build_tag_index(index)
|
||||||
|
|
||||||
|
tag_selector_options = []
|
||||||
|
tag_selector_css_rules = [f'''
|
||||||
|
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.
|
||||||
|
for tag, articles in sorted(tag_index.items(), key = lambda item : -len(item[1])):
|
||||||
|
tag_selector_options.append(format_html_template(
|
||||||
|
template_selections['tag_selector_option'],
|
||||||
|
tag_name = tag,
|
||||||
|
number_with_tag = len(articles),
|
||||||
|
site = site,
|
||||||
|
**kwargs
|
||||||
|
))
|
||||||
|
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}"]){{{{
|
||||||
|
display: none;
|
||||||
|
}}}}
|
||||||
|
body:has(input[name="tag-selector"][value="{tag}"]:checked) li:has(.blog-tag[data="{tag}"]){{{{
|
||||||
|
display: list-item!important;
|
||||||
|
}}}}
|
||||||
|
''')
|
||||||
|
|
||||||
|
# For Python 3.9-friendliness.
|
||||||
|
newline = '\n'
|
||||||
|
|
||||||
|
# Generate the archive article.
|
||||||
|
archive_html_article = format_html_template(
|
||||||
|
template_selections['archive_article'],
|
||||||
|
content = archive_html_list,
|
||||||
|
tag_selector_options = ' '.join(tag_selector_options),
|
||||||
|
tag_selector_css_rules = f'<style>{newline.join(tag_selector_css_rules)}</style>',
|
||||||
|
site = site,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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_rss_feed(site: SiteConfig, index: dict[str, Article]):
|
||||||
|
feed = rfeed.Feed(
|
||||||
|
title = site.title,
|
||||||
|
link = f"{site.base_url.rstrip('/')}/rss.xml",
|
||||||
|
description = site.description,
|
||||||
|
language = "en-US",
|
||||||
|
lastBuildDate = datetime.now(),
|
||||||
|
items = [
|
||||||
|
rfeed.Item(
|
||||||
|
title = article.metadata.title,
|
||||||
|
link = f"{site.base_url.rstrip('/')}/{article.path}",
|
||||||
|
description = article.metadata.description,
|
||||||
|
author = article.metadata.author,
|
||||||
|
guid = rfeed.Guid(article.path),
|
||||||
|
pubDate = datetime(
|
||||||
|
article.metadata.date.year,
|
||||||
|
article.metadata.date.month,
|
||||||
|
article.metadata.date.day
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for article in index.values()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(f"{site.web_root.rstrip('/')}/rss.xml", 'w') as f:
|
||||||
|
f.write(feed.rss())
|
||||||
78
jimsite/common.py
Normal file
78
jimsite/common.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import os
|
||||||
|
import inspect
|
||||||
|
import subprocess
|
||||||
|
import pydantic
|
||||||
|
from dotmap import DotMap
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
run = lambda cmd: subprocess.run(
|
||||||
|
cmd.split(' '),
|
||||||
|
stdout = subprocess.PIPE,
|
||||||
|
stderr = subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
def filepath_or_string(s: str) -> str:
|
||||||
|
'''Loads the contents of a string if it is a filepath, otherwise returns the string.'''
|
||||||
|
if os.path.isfile(s):
|
||||||
|
with open(s, 'r') as f:
|
||||||
|
return f.read()
|
||||||
|
else:
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalVars(pydantic.BaseModel):
|
||||||
|
'''Static-valued global variables to be interpolated into any HTML templates.'''
|
||||||
|
today: date = datetime.today()
|
||||||
|
|
||||||
|
class GitRepo(pydantic.BaseModel):
|
||||||
|
'''A nested model used in SiteConfig to represent a Git repo.'''
|
||||||
|
url: str
|
||||||
|
branch: Optional[str] = None
|
||||||
|
remote: Optional[str] = 'origin'
|
||||||
|
|
||||||
|
class SiteConfig(pydantic.BaseModel):
|
||||||
|
base_url: str
|
||||||
|
web_root: str
|
||||||
|
build_cache: str
|
||||||
|
title: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
published: Optional[bool] = True
|
||||||
|
git_repo: Optional[GitRepo] = None
|
||||||
|
assets: Optional[list] = None
|
||||||
|
articles: Optional[list] = None
|
||||||
|
template_selections: Optional[dict] = {}
|
||||||
|
addons: Optional[list] = None
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
5
jimsite/requirements.txt
Normal file
5
jimsite/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
ipykernel
|
||||||
|
markdown
|
||||||
|
pyyaml
|
||||||
|
rfeed
|
||||||
|
dotmap
|
||||||
154
jimsite/templating.py
Normal file
154
jimsite/templating.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
from dotmap import DotMap
|
||||||
|
from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
# 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')}
|
||||||
|
```
|
||||||
|
'''
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
def format_html_template(template: str, **kwargs) -> str:
|
||||||
|
'''Interpolates variables specified as keyword arguments
|
||||||
|
into the given HTML template.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Load the template if a filepath is given.
|
||||||
|
template = filepath_or_string(template)
|
||||||
|
|
||||||
|
# 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 formatted_html
|
||||||
|
|
||||||
|
|
||||||
|
def map_templates(dir: str, parent = '') -> DotMap:
|
||||||
|
'''Recursively maps the templates directory into a nested dict structure.
|
||||||
|
Leaves map the filestems of .html template files to their contents.
|
||||||
|
'''
|
||||||
|
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
# List the files and subdirectories at the top level.
|
||||||
|
for sub in os.listdir(os.path.join(parent,dir)):
|
||||||
|
|
||||||
|
# Construct the full path to the file or subdir from the root of the tree.
|
||||||
|
full_path = os.path.join(parent,dir,sub)
|
||||||
|
|
||||||
|
# Recursively map subdirectories.
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
output[sub] = map_templates(sub, parent = dir)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Templates must be .html files.
|
||||||
|
filestem, ext = os.path.splitext(sub)
|
||||||
|
if ext != '.html':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Load template file.
|
||||||
|
with open(full_path, 'r') as file:
|
||||||
|
html = file.read()
|
||||||
|
|
||||||
|
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',
|
||||||
|
tag_selector = 'templates.components.blog_archive_tag_selector',
|
||||||
|
tag_selector_option = 'templates.components.blog_archive_tag_selector_option'
|
||||||
|
) | (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]))
|
||||||
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<article>
|
|
||||||
<h1>Welcome!</h1>
|
|
||||||
<hr />
|
|
||||||
<p>Welcome to my little corner of the Internet! My name is Jim <span title="'ʃɛpɪk">Shepich</span> (@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."</p>
|
|
||||||
|
|
||||||
<p>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 <i>new game plus</i>). And if some other kindred spirits find something here that helps them on their journey, that's a bonus! </p>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
@ -1,177 +0,0 @@
|
|||||||
if(document.getElementsByName("keywords")[0].content.indexOf("player")>-1){
|
|
||||||
main();
|
|
||||||
}
|
|
||||||
|
|
||||||
function main(){
|
|
||||||
var tables = document.getElementsByTagName("table");
|
|
||||||
var abilities = {"-":0};
|
|
||||||
var skills = {};
|
|
||||||
var stats = {};
|
|
||||||
var features = {};
|
|
||||||
var masteries = [];
|
|
||||||
var EXP = {};
|
|
||||||
|
|
||||||
for(i=0;i<tables.length;i++){
|
|
||||||
var header_names = "";
|
|
||||||
var header_row = tables[i].children[0].children[0];
|
|
||||||
//Iterate over the thead -> tr -> th elements
|
|
||||||
for(k=0;k<header_row.children.length;k++){
|
|
||||||
header_names += header_row.children[k].innerHTML.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(checkheaders(header_names,["Ability","Score","Modifier"])==3){
|
|
||||||
abilities.DOM = tables[i];
|
|
||||||
} else if(checkheaders(header_names,["Ability","Skill","Bonus","EXP"])==4){
|
|
||||||
skills.DOM = tables[i];
|
|
||||||
} else if(checkheaders(header_names,["Stat","Value"])==2){
|
|
||||||
stats.DOM = tables[i];
|
|
||||||
} else if(checkheaders(header_names,["Feature","EXP","Description"])==3){
|
|
||||||
features.DOM = tables[i];
|
|
||||||
} else if(checkheaders(header_names,["Mastery","EXP","Requirements","Type","Description"])==5){
|
|
||||||
masteries.push({"DOM":tables[i]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkheaders(string, keywords){
|
|
||||||
var output = 0;
|
|
||||||
for(j=0;j<keywords.length;j++){
|
|
||||||
if(string.indexOf(keywords[j])>-1){
|
|
||||||
output += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function align(cell,how="center"){
|
|
||||||
cell.className = cell.className.replace("leftalign","");
|
|
||||||
cell.className = cell.className.replace("rightalign","");
|
|
||||||
cell.className = cell.className.replace("centeralign","");
|
|
||||||
cell.className += " "+how+"align";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
for(i=0;i<abilities.DOM.children[1].children.length;i++){
|
|
||||||
//Iterate over the rows of the abilities table
|
|
||||||
var row = abilities.DOM.children[1].children[i];
|
|
||||||
var ability = row.children[0].innerHTML.trim();
|
|
||||||
abilities[ability] = parseInt(row.children[2].innerHTML.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
var row_modifier = "";
|
|
||||||
for(i=0;i<skills.DOM.children[1].children.length;i++){
|
|
||||||
//Iterate over rows of the skills table.
|
|
||||||
var row = skills.DOM.children[1].children[i];
|
|
||||||
var bonus;
|
|
||||||
var exp;
|
|
||||||
if(row.children[0].innerHTML.trim() in abilities){
|
|
||||||
row_modifier = row.children[0].innerHTML.trim();
|
|
||||||
skill = row.children[1];
|
|
||||||
bonus = row.children[2];
|
|
||||||
exp = row.children[3];
|
|
||||||
} else {
|
|
||||||
skill = row.children[0];
|
|
||||||
bonus = row.children[1];
|
|
||||||
exp = row.children[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isNaN(parseInt(exp.innerHTML.trim()))){
|
|
||||||
exp.innerHTML = "0";
|
|
||||||
}
|
|
||||||
align(exp);
|
|
||||||
align(bonus);
|
|
||||||
|
|
||||||
EXP[skill.innerHTML.trim()] = parseInt(exp.innerHTML.trim());
|
|
||||||
var skill_bonus = Math.floor(parseInt(exp.innerHTML.trim())/2) + abilities[row_modifier];
|
|
||||||
bonus.innerHTML = " "+skill_bonus+" ";
|
|
||||||
skills[skill.innerHTML.trim()] = skill_bonus;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(i=0;i<stats.DOM.children[1].children.length;i++){
|
|
||||||
var row = stats.DOM.children[1].children[i];
|
|
||||||
var stat = row.children[0];
|
|
||||||
var value = row.children[1];
|
|
||||||
var exp = row.children[2];
|
|
||||||
align(value);
|
|
||||||
align(exp);
|
|
||||||
|
|
||||||
stats[stat.innerHTML.trim()] = value;
|
|
||||||
|
|
||||||
var exp_value = exp.innerHTML.trim();
|
|
||||||
|
|
||||||
if(isNaN(exp_value)){
|
|
||||||
if(exp_value=="-"){
|
|
||||||
exp_value = 0;
|
|
||||||
} else if(exp_value.indexOf("d")>-1) {
|
|
||||||
stats["HD Improvement"] = parseInt(exp_value.substring(exp_value.indexOf("d")+1,exp_value.length));
|
|
||||||
stats["HD Increase"] = parseInt(exp_value.substring(0,exp_value.indexOf("d")));
|
|
||||||
exp_value = stats["HD Improvement"]+stats["HD Increase"];
|
|
||||||
} else {
|
|
||||||
exp_value = 0;
|
|
||||||
exp.innerHTML = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
EXP[stat.innerHTML.trim()] = exp_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(i=0;i<features.DOM.children[1].children.length;i++){
|
|
||||||
var row = features.DOM.children[1].children[i];
|
|
||||||
var feature = row.children[0];
|
|
||||||
var exp_str = row.children[1].innerHTML.trim();
|
|
||||||
var exp = parseInt(exp_str.substring(0,exp_str.indexOf("/")));
|
|
||||||
features[feature.innerHTML.trim()] = parseInt(exp_str.substring(exp_str.indexOf("/")+1,exp_str.length));
|
|
||||||
EXP[feature.innerHTML.trim()] = exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(j=0;j<masteries.length;j++){
|
|
||||||
var mastery = masteries[j];
|
|
||||||
for(i=0;i<mastery.DOM.children[1].children.length;i++){
|
|
||||||
var row = mastery.DOM.children[1].children[i];
|
|
||||||
var feature = row.children[0];
|
|
||||||
var exp_str = row.children[1].innerHTML.trim();
|
|
||||||
var exp = 0;
|
|
||||||
if(exp_str.indexOf("/")>-1){
|
|
||||||
exp = parseInt(exp_str.substring(0,exp_str.indexOf("/")));
|
|
||||||
} else {
|
|
||||||
exp = parseInt(exp_str);
|
|
||||||
}
|
|
||||||
EXP[feature.innerHTML.trim()] = exp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var HP = 1 + Math.floor((skills["Persistence"]+skills["Toughness"])/2);
|
|
||||||
if(HP<1){
|
|
||||||
HP = 1;
|
|
||||||
}
|
|
||||||
for(h=1;h<=EXP["HP"];h++){
|
|
||||||
if(h%5==0 && abilities["CON"]>=1){
|
|
||||||
HP += abilities["CON"];
|
|
||||||
} else {
|
|
||||||
HP += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stats["HP"].innerHTML = " "+HP+" ";
|
|
||||||
|
|
||||||
if(!("HD Increase" in stats) && !("HD Improvement" in stats)){
|
|
||||||
stats["HD Increase"] = 0;
|
|
||||||
stats["HD Improvement"] = 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
var hd = [Math.floor(stats["HD Increase"]/2)+1];
|
|
||||||
hd.push(4+2*Math.floor(stats["HD Improvement"]/2));
|
|
||||||
stats["Hit Dice"].innerHTML = hd[0]+"d"+hd[1];
|
|
||||||
|
|
||||||
|
|
||||||
var AC = 8 + Math.ceil(skills["Toughness"]/3) + Math.ceil(skills["Evasion"]/3);
|
|
||||||
stats["AC"].innerHTML = " "+AC+" ";
|
|
||||||
|
|
||||||
var exp_total = [0,parseInt(stats["EXP Total"].innerHTML.trim())];
|
|
||||||
for(key in EXP){
|
|
||||||
var temp_exp = parseInt(EXP[key]);
|
|
||||||
if(!isNaN(temp_exp)){
|
|
||||||
exp_total[0] += temp_exp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stats["EXP Total"].innerHTML = (exp_total[1]-exp_total[0])+"/"+exp_total[1];
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
$socials_string = file_get_contents(__DIR__ ."/../data/socials.json",true);
|
|
||||||
//__DIR__ constant specifies the path to the script
|
|
||||||
$socials = json_decode($socials_string);
|
|
||||||
|
|
||||||
foreach($socials as $s){
|
|
||||||
echo "<a class='social' href='" . $s->link . "'><span class='fa-brands fa-" . $s->icon . "' title='".$s->platform."'></span></a>";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "<br /><span class='copyright'>Copyright © 2021-".date("Y")." Jim Shepich</span>";
|
|
||||||
?>
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
function extract_data(){
|
|
||||||
var data = document.getElementsByTagName("tr");
|
|
||||||
var master = {};
|
|
||||||
var header = null;
|
|
||||||
for(row of data){
|
|
||||||
if(row.className == "row0"){
|
|
||||||
continue;
|
|
||||||
//Skip table headers
|
|
||||||
} else if (row.className == "row1"){
|
|
||||||
header = row.parentElement.parentElement.parentElement.parentElement.previousElementSibling.innerText;
|
|
||||||
//If we're at a new table, then we have to go up the DOM tree until we
|
|
||||||
//find that section's header.
|
|
||||||
master[header] = [];
|
|
||||||
//And create a new list for that header in the master object.
|
|
||||||
}
|
|
||||||
|
|
||||||
var row_object = {};
|
|
||||||
row_object["link"] = row.children[0].children[0].href;
|
|
||||||
//The link can be found in the anchor element in the first cell of the
|
|
||||||
//row.
|
|
||||||
var text = row.children[0].children[0].innerText;
|
|
||||||
//Get the text of the link.
|
|
||||||
row_object["song"] = text.substring(0,text.indexOf("(")-1);
|
|
||||||
//The name of the song is the start of the text until the space before
|
|
||||||
//the parenthesis. I've made sure to use hyphens in place of parentheses in
|
|
||||||
//song titles.
|
|
||||||
row_object["source"] = text.substring(text.indexOf("(")+1, text.indexOf(")"));
|
|
||||||
//The work that the song is from is indicated between the parentheses.
|
|
||||||
|
|
||||||
row_object["vibes"] = row.children[1].innerText.split(", ")
|
|
||||||
//The vibes are in the second column, delimited by a comma-space.
|
|
||||||
|
|
||||||
|
|
||||||
master[header].push(row_object);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return master;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function download(filename, text) {
|
|
||||||
//Borrowed shamelessly from:
|
|
||||||
//https://ourcodeworld.com/articles/read/189/how-to-create-a-file-and-generate-a-download-with-javascript-in-the-browser-without-a-server
|
|
||||||
var element = document.createElement('a');
|
|
||||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
|
||||||
element.setAttribute('download', filename);
|
|
||||||
|
|
||||||
element.style.display = 'none';
|
|
||||||
document.body.appendChild(element);
|
|
||||||
|
|
||||||
element.click();
|
|
||||||
|
|
||||||
document.body.removeChild(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
var button = document.getElementById("download-JSON");
|
|
||||||
button.addEventListener("click",function(){
|
|
||||||
var json_data = JSON.stringify(extract_data(),null,2);
|
|
||||||
download("epics_bgm.json",json_data);
|
|
||||||
});
|
|
||||||
385
scripts/lists.js
385
scripts/lists.js
@ -1,385 +0,0 @@
|
|||||||
//Variale `list` is imported from JSON with query_handler.php.
|
|
||||||
//var lists = JSON.parse(document.getElementById("lists-json").innerText);
|
|
||||||
var list_id = document.getElementById("query-list").innerText;
|
|
||||||
//Get list id from <var> tag created in query-handler.php.
|
|
||||||
var selected_list = null;
|
|
||||||
if(lists.hasOwnProperty(list_id)){
|
|
||||||
selected_list = lists[list_id];
|
|
||||||
} else {
|
|
||||||
selected_list = lists["master"];
|
|
||||||
//If the list id in the query is invalid, go back to the main list.
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function gen_list_html(list){
|
|
||||||
//This function creates HTML from a list's JSON object by iterativel calling the gen_item_html function to convert individual items. This design paradigm facilitates the construction of nested lists or lists with sections.
|
|
||||||
var html = "";
|
|
||||||
if(!list.hasOwnProperty("type")){
|
|
||||||
list.type = "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!list.hasOwnProperty("subtype")){
|
|
||||||
list.subtype = "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
var list_type = list.type;
|
|
||||||
|
|
||||||
if(list.hasOwnProperty("sections")){
|
|
||||||
list_type = "sectioned";
|
|
||||||
//Lists that have separate sections require special handling involving recursion.
|
|
||||||
}
|
|
||||||
|
|
||||||
switch(list_type){
|
|
||||||
case "master":
|
|
||||||
for(id in lists){
|
|
||||||
html += gen_item_html(id,type="list-id");
|
|
||||||
}
|
|
||||||
html = `<ul>${html}</ul>`;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "quotes":
|
|
||||||
html += "<hr />";
|
|
||||||
for(quote of list.list){
|
|
||||||
html += gen_item_html(quote,type="quote");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "key-value":
|
|
||||||
for(pair of list.list){
|
|
||||||
html += gen_item_html(pair,type="kv-pair");
|
|
||||||
}
|
|
||||||
html = (list.hasOwnProperty("ordered") && list.ordered) ? `<ol>${html}</ol>` : `<ul>${html}</ul>`;
|
|
||||||
//Create an ordered list if list has the property "ordered".
|
|
||||||
break;
|
|
||||||
|
|
||||||
|
|
||||||
case "gallery":
|
|
||||||
document.getElementById("lists").style.textAlign="center";
|
|
||||||
//Center the rows of exhibits on the page.
|
|
||||||
if(!list.hasOwnProperty("subtype")){
|
|
||||||
list.subtype="default";
|
|
||||||
}
|
|
||||||
for(exhibit of list.list){
|
|
||||||
html += gen_item_html(exhibit,type="exhibit",subtype=list.subtype);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "external":
|
|
||||||
//This branch will only run if someone manually enters the list name into the query; clicking an external-type list from the Master List will just open the link in a new tab.
|
|
||||||
setTimeout(function(){location.assign(list["link"])}, 600);
|
|
||||||
//If the list specified in the query has the external property, then redirect to the external link. I use setTimeout in order to allow the rest of the code to run.
|
|
||||||
list.title = `Redirecting to List of ${list.title}...`;
|
|
||||||
list.list = [];
|
|
||||||
//It takes a second to redirect, so put some filler on the page while the reader waits.
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "sectioned":
|
|
||||||
if(!list.hasOwnProperty("section-level")||list["section-level"]==null){
|
|
||||||
list["section-level"] = 1;
|
|
||||||
//Section-level defines the heading level of the section. Top level is 1.
|
|
||||||
}
|
|
||||||
if(!list.hasOwnProperty("dropdown")||list.dropdown==null){
|
|
||||||
list.dropdown = true;
|
|
||||||
//By default, sections ARE in details/summary tags.
|
|
||||||
}
|
|
||||||
if(!list.hasOwnProperty("dropdown-open")||list["dropdown-open"]==null){
|
|
||||||
list["dropdown-open"] = false;
|
|
||||||
//By default, sections are collapsed.
|
|
||||||
}
|
|
||||||
for(section of list.sections){
|
|
||||||
var section_html = "";
|
|
||||||
var level = list["section-level"] + 1;
|
|
||||||
section["section-level"] = level;
|
|
||||||
//Nested secions are of lower levels.
|
|
||||||
if(!section.hasOwnProperty("type")||section.type==undefined){
|
|
||||||
section.type = list.type;
|
|
||||||
//This branch transfers the list type down from higher levels. By default, the bottom-level lists will inherit the type of the top-level object unless otherwise specified.
|
|
||||||
}
|
|
||||||
if(!section.hasOwnProperty("subtype")||section.subtype==undefined){
|
|
||||||
section.subtype = list.subtype;
|
|
||||||
//Sections should also inherit subtypes.
|
|
||||||
}
|
|
||||||
if(!section.hasOwnProperty("dropdown")||section.dropdown==undefined){
|
|
||||||
section.dropdown = list.dropdown;
|
|
||||||
//Inherit dropdown-ness unless otherwise specified.
|
|
||||||
}
|
|
||||||
if(!section.hasOwnProperty("dropdown-open")||section["dropdown-open"]==undefined){
|
|
||||||
section["dropdown-open"] = list["dropdown-open"];
|
|
||||||
//Inherit whether the dropdown should be collapsed or open.
|
|
||||||
}
|
|
||||||
var description = (section.hasOwnProperty("description")) ? `<p>${section.description}</p>` : "";
|
|
||||||
//Sections can have their own descriptions.
|
|
||||||
|
|
||||||
var title = `<h${level}>${section.title}</h${level}>`;
|
|
||||||
//Wrap the section title in a header tag.
|
|
||||||
|
|
||||||
if(section.hasOwnProperty("dropdown") && section.dropdown){
|
|
||||||
//If the section is marked with the "dropdown" attribute, then nest the section's data in a details/summary tag.
|
|
||||||
var open = (section["dropdown-open"]) ? "open":"";
|
|
||||||
section_html = `<details class="heading" ${open}><summary class="heading">${title}</summary>${description}${gen_list_html(section)}</details>`;
|
|
||||||
} else {
|
|
||||||
section_html = `${title}${description}${gen_list_html(section)}`;
|
|
||||||
}
|
|
||||||
//Sectioned
|
|
||||||
html += `<section id="${section.title}">${section_html}</section>`;
|
|
||||||
//Assigning ids allows for the possibility of fragment linking.
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
for(item of list.list){
|
|
||||||
html += gen_item_html(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
html = (list.hasOwnProperty("ordered") && list.ordered) ? `<ol>${html}</ol>` : `<ul>${html}</ul>`;
|
|
||||||
//Create an ordered list if list has the property "ordered".
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function gen_item_html(item,type="default",subtype=null){
|
|
||||||
var item_html = "";
|
|
||||||
switch(type){
|
|
||||||
case "list-id":
|
|
||||||
if(lists[item].hasOwnProperty("hidden") && lists[item].hidden){
|
|
||||||
return "";
|
|
||||||
//Lists marked with the "hidden" attribute are in-development and should not be displayed on the Master List.
|
|
||||||
}
|
|
||||||
if(!lists[item].hasOwnProperty("title")){
|
|
||||||
lists[item].title = item;
|
|
||||||
//If a title is not set for the list, then just use its id.
|
|
||||||
}
|
|
||||||
var tooltip = "";
|
|
||||||
if(lists[item].hasOwnProperty("description")){
|
|
||||||
tooltip=lists[item]["description"];
|
|
||||||
//When hovering over a list in the directory, display its description as a tooltip.
|
|
||||||
}
|
|
||||||
var link = `href='index.php?page=lists&list=${item}' target='_self'`;
|
|
||||||
//By default, lists have internal links that change the query value to that list's title. Internal links should open in the same tab.
|
|
||||||
if(lists[id].hasOwnProperty("type") && lists[item].type=="external"){
|
|
||||||
link = `href='${lists[item]["link"]}' target='_blank'`;
|
|
||||||
//For external lists, use their link instead of an internal link. External links should open in a new tab.
|
|
||||||
}
|
|
||||||
item_html = `<li title="${tooltip}"><a ${link}>${lists[item].title}</a></li>`;
|
|
||||||
//The Master List contains a link to every other list in the JSON file.
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "quote":
|
|
||||||
//Format quotes like they are formatted on Goodreads.
|
|
||||||
if(!item.hasOwnProperty("quote")){
|
|
||||||
//If the quote is blank, then things are going to get real weird. Most likely, this will be caused by a typo, so this branch is a safeguard against any resulting errors.
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
item_html = "<p>";
|
|
||||||
if(item.hasOwnProperty("title")){
|
|
||||||
item_html += `<b>${item.title}</b><br />`;
|
|
||||||
//Add a title if the quote has one (e.g. "The Litany Against Fear").
|
|
||||||
}
|
|
||||||
item_html += `“${item.quote}”<br>`;
|
|
||||||
//Add the text of the quote.
|
|
||||||
if(item.hasOwnProperty("card") && !item.hasOwnProperty("quotee")){
|
|
||||||
item.quotee="";
|
|
||||||
//If a flavor text doesn't have a quotee, don't write "Unknown".
|
|
||||||
}
|
|
||||||
|
|
||||||
item_html += ` — ${item.hasOwnProperty("quotee") ? item.quotee : "Unknown"}`;
|
|
||||||
//Add the quotee's name, or "Unknown" if one is not specified.
|
|
||||||
if(item.hasOwnProperty("source")){
|
|
||||||
if(item.hasOwnProperty("quotee") && item.quotee!=""){
|
|
||||||
item_html += ", ";
|
|
||||||
//Unless there is no quotee, separate the quotee from the source with ", "
|
|
||||||
}
|
|
||||||
item_html += item.source;
|
|
||||||
//Add the source if the quote has one.
|
|
||||||
}
|
|
||||||
|
|
||||||
if(item.hasOwnProperty("card")){
|
|
||||||
if(item.quotee!=""||item.hasOwnProperty("source")){
|
|
||||||
item_html += `, `;
|
|
||||||
//Separate quotee or source from card title with ", "
|
|
||||||
}
|
|
||||||
item_html += `<a target='_blank' href='https://gatherer.wizards.com/pages/card/Details.aspx?multiverseid=${item.multiverseid}'>${item.card}</a> (Magic: the Gathering)`;
|
|
||||||
}
|
|
||||||
item_html += "</p><hr />";
|
|
||||||
//Delimit the quotes with a horizontal rule.
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "kv-pair":
|
|
||||||
if(!(item.hasOwnProperty("k")&&item.hasOwnProperty("v"))||Object.keys(item).length==1){
|
|
||||||
var key = Object.keys(item)[0];
|
|
||||||
item["k"] = key;
|
|
||||||
item["v"] = item[key];
|
|
||||||
//If the item is an object containing a single key-value pair, then use that pair as the k and v for displaying.
|
|
||||||
}
|
|
||||||
if(item.hasOwnProperty("link")){
|
|
||||||
item_html = `<span class="list-key"><a href="${item.link}" target="_blank">${item["k"]}</a></span>`;
|
|
||||||
} else {
|
|
||||||
item_html = `<span class="list-key">${item["k"]}</span>`;
|
|
||||||
}
|
|
||||||
item_html += ` — ${item["v"]}`;
|
|
||||||
|
|
||||||
if(item.hasOwnProperty("list") && Array.isArray(item.list)){
|
|
||||||
//Sublist time, baby!
|
|
||||||
var temp = {
|
|
||||||
"title":item_html,
|
|
||||||
"list":item.list,
|
|
||||||
"ordered": (item.hasOwnProperty("ordered")) ? item.ordered:undefined,
|
|
||||||
"dropdown": (item.hasOwnProperty("dropdown")) ? item.dropdown:undefined,
|
|
||||||
"dropdown-open": (item.hasOwnProperty("dropdown-open")) ? item["dropdown-open"]:undefined
|
|
||||||
};
|
|
||||||
//If a key-value pair has a list key with an array attribute, then reformat it as a sublist whose title is the kv pair and whose list is the array.
|
|
||||||
item_html = gen_item_html(temp,type="sublist")
|
|
||||||
} else {
|
|
||||||
item_html = `<li>${item_html}</li>`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
|
|
||||||
case "exhibit":
|
|
||||||
var tooltip, alt, text, img_src, image;
|
|
||||||
tooltip = text = img_src = image = "";
|
|
||||||
classes = {"div":[],"img":[],"a":[]}
|
|
||||||
switch(subtype){
|
|
||||||
case "album":
|
|
||||||
tooltip = `${item.title} (${item.year}) - ${item.artist}`;
|
|
||||||
alt = tooltip;
|
|
||||||
img_src = item.cover;
|
|
||||||
text = `${item.title}<br />${item.artist}<br />(${item.year})`;
|
|
||||||
break;
|
|
||||||
case "movie":
|
|
||||||
tooltip = `${item.title} (${item.year})`;
|
|
||||||
alt = tooltip;
|
|
||||||
img_src = item.poster;
|
|
||||||
text = `${item.title}<br />(${item.year})`;
|
|
||||||
break;
|
|
||||||
case "mtg-card":
|
|
||||||
var gatherer_link = "https://gatherer.wizards.com/pages/card/Details.aspx?multiverseid=";
|
|
||||||
var gatherer_image = "https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=";
|
|
||||||
|
|
||||||
if(item.hasOwnProperty("multiverseid")&&!item.hasOwnProperty("link")){
|
|
||||||
if(Array.isArray(item.multiverseid)){
|
|
||||||
//If the multiverseid is an array, treat it as a transform card and link to the Gatherer page for the front half.
|
|
||||||
item.link = gatherer_link+item.multiverseid[0];
|
|
||||||
} else {
|
|
||||||
item.link = gatherer_link+item.multiverseid;
|
|
||||||
}
|
|
||||||
//If there's no alternate link specified, then use the multiverseid to generate the Gatherer link.
|
|
||||||
}
|
|
||||||
if(item.hasOwnProperty("multiverseid")&&!item.hasOwnProperty("image")){
|
|
||||||
if(Array.isArray(item.multiverseid)){
|
|
||||||
img_src = [];
|
|
||||||
for(id of item.multiverseid){
|
|
||||||
img_src.push(gatherer_image+id+"&type=card")
|
|
||||||
}
|
|
||||||
//If there are multiple (two) multiverseids, make a link src for each of them for convenience.
|
|
||||||
} else {
|
|
||||||
img_src = gatherer_image+item.multiverseid+"&type=card";
|
|
||||||
}
|
|
||||||
//If there's no alternate link specified, then use the multiverseid to generate the Gatherer link for the image.
|
|
||||||
} else {
|
|
||||||
if(Array.isArray(item.image)){
|
|
||||||
img_src=[];
|
|
||||||
for(src of item.image){
|
|
||||||
img_src.push(src);
|
|
||||||
}
|
|
||||||
//If multiple (two) image sources are specified, create an array of both.
|
|
||||||
} else {
|
|
||||||
img_src = item.image;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(item.hasOwnProperty("name")){
|
|
||||||
alt = tooltip = item.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Array.isArray(img_src)){
|
|
||||||
image = `<img title="${tooltip}" alt="${tooltip}" src="${img_src[0]}" class="gallery exhibit-${subtype} card-front ${classes.img.join(" ")}" /><img title="${tooltip}" alt="${tooltip}" src="${img_src[1]}" class="gallery exhibit-${subtype} card-back ${classes.img.join(" ")}" />`
|
|
||||||
//Two srcs are to be treated as those of a transform card, which switches on hover.
|
|
||||||
classes.div.push("card-transform");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
if(image==""||!image){
|
|
||||||
image = image = `<img title="${tooltip}" alt="${tooltip}" src="${img_src}" class="gallery exhibit-${subtype} ${classes.img.join(" ")}" />`;
|
|
||||||
//If the image variable is not already defined, then generate it.
|
|
||||||
}
|
|
||||||
//Gallery items must have an image.
|
|
||||||
if(item.hasOwnProperty("link")){
|
|
||||||
image = `<a href='${item.link}' target='_blank' class="${classes.a.join(" ")}">${image}</a>`;
|
|
||||||
//If there's a link associated with the exhibit, put it on the image.
|
|
||||||
}
|
|
||||||
item_html = (text=="") ? image : `${image}<br />${text}`;
|
|
||||||
//If there's no text, then there's no need for a line break.
|
|
||||||
item_html = `<div class="gallery exhibit-${subtype} ${classes.div.join(" ")}">${item_html}</div>`
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "sublist":
|
|
||||||
//Sublists can be any type (most likely default or key-value), so the best way to deal with them is recursion.
|
|
||||||
if(item.hasOwnProperty("dropdown") && item.dropdown){
|
|
||||||
var open = (item.hasOwnProperty("dropdown-open")&&item["dropdown-open"])?"open":"";
|
|
||||||
item_html = `<li><details ${open}><summary>${item.title}</summary>${gen_list_html(item)}</details></li>`;
|
|
||||||
} else {
|
|
||||||
item_html = `<li>${item.title}${gen_list_html(item)}</li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if(["string","number"].includes(typeof item)){
|
|
||||||
item_html = `<li>${item}</li>`;
|
|
||||||
//If the element is a simple string or number, then we don't need to do any special formatting.
|
|
||||||
} else if (typeof item === "object"){
|
|
||||||
var keys = Object.keys(item);
|
|
||||||
if(keys.length == 1){
|
|
||||||
if (Array.isArray(item[keys[0]])){
|
|
||||||
//An item that is a dictionary only containing a list is probably a sublist. Format it as such, and pass it back through this switch statement.
|
|
||||||
var temp = {
|
|
||||||
"title":keys[0],
|
|
||||||
"type":"default",
|
|
||||||
"list":item[keys[0]]
|
|
||||||
};
|
|
||||||
item_html = gen_item_html(temp,"sublist");
|
|
||||||
} else {
|
|
||||||
item_html = gen_item_html(item,"kv-pair");
|
|
||||||
//A item that is dictionary with one key, whose value is not a list, is to be treated as a term-explanation type key-value pair.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
item_html = gen_item_html(item,"sublist");
|
|
||||||
//At this point, if there is no other specification, an item that's an object is probably a sublist.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item_html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function str2html(md){
|
|
||||||
//This function replaces escapes commands and Markdown with their HTML equivalents.
|
|
||||||
html = md.replaceAll("\n","<br />");
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
document.getElementById("list-title").innerHTML = selected_list.title;
|
|
||||||
if(selected_list.hasOwnProperty("description")){
|
|
||||||
document.getElementById("list-description").innerHTML = selected_list.description;
|
|
||||||
} else {
|
|
||||||
document.getElementById("list-description").style.display = "none";
|
|
||||||
//If the list has no description, hide the element to remove the awkward space from the padding.
|
|
||||||
}
|
|
||||||
//Generate the article header from the list's title and description.
|
|
||||||
|
|
||||||
document.getElementById("list-container").innerHTML += str2html(gen_list_html(selected_list));
|
|
||||||
//Call the gen_list_html function to convert the list from a JSON object to sophisticated HTML.
|
|
||||||
|
|
||||||
if(list_id != "master"){
|
|
||||||
document.getElementById("lists").innerHTML += "<br /><p><a id='list-return-link' href='index.php?page=lists&list=master'>Return to Master List ↩</a></p>";
|
|
||||||
//Add a return link to the bottom of the article.
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
function page_comparator($a,$b){
|
|
||||||
//This function is the sorting criterion for the later call of usort, which will sort the page objects
|
|
||||||
//from pages.json in increasing order of their "index" value.
|
|
||||||
return $a->index <=> $b->index;
|
|
||||||
//For some reason, PHP8 requires the spaceship operator (<=>)
|
|
||||||
//instead of a greater than symbol.
|
|
||||||
}
|
|
||||||
|
|
||||||
function gen_nav_element($page){
|
|
||||||
$iframe = false;
|
|
||||||
//By default, echo the contents of a file instead of embedding it in an iframe.
|
|
||||||
if(isset($page->file)){
|
|
||||||
$href = "index.php?page=".$page->query_value;
|
|
||||||
$target = "_self";
|
|
||||||
//If the page is associated with a file, then point the navibar href to the file's query value.
|
|
||||||
|
|
||||||
$iframe = (isset($page->iframe)) ? true : false;
|
|
||||||
//If the page has the iframe attribute, then keep track of that.
|
|
||||||
|
|
||||||
} elseif(isset($page->link)) {
|
|
||||||
$href = $page->link;
|
|
||||||
$target = "_blank";
|
|
||||||
//If instead the page is associated with an external link, then point the navibar href there and make it open in a new window.
|
|
||||||
}
|
|
||||||
echo "<a href='".$href."' target='".$target."'><div class='nav-tab'><span class='nav-text'>".$page->name."</span></div></a>";
|
|
||||||
}
|
|
||||||
|
|
||||||
$page_array = get_object_vars($pages);
|
|
||||||
//Convert $pages from object to an associative array of [key=>val, key=>val]
|
|
||||||
//where the keys are the names of the page objects from the JSON file and the
|
|
||||||
//vals are the page objects themselves.
|
|
||||||
usort($page_array,"page_comparator");
|
|
||||||
//Sort the pages in order of increasing "index" value.
|
|
||||||
|
|
||||||
foreach($page_array as $key=>$p){
|
|
||||||
if($p->index>-1){
|
|
||||||
gen_nav_element($p);
|
|
||||||
//Pages with indices less than 0 are hidden from the navibar.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
//__DIR__ points to scripts folder.
|
|
||||||
$pages = json_decode(file_get_contents(__DIR__ ."/../data/pages.json"));
|
|
||||||
//Load dictionary of site pages from pages.json file.
|
|
||||||
$query_page = (isset($_REQUEST['page']) && !empty($_REQUEST['page'])) ? $_GET['page'] : "home";
|
|
||||||
//Get value of "page" query key; default is "home".
|
|
||||||
$page = (isset($pages->$query_page)) ? $pages->$query_page : $pages->{404};
|
|
||||||
//If the query value ($query_page) is not in the dictionary ($pages), then load the 404 page instead.
|
|
||||||
echo "<var id='query-page'>".$query_page."</var>";
|
|
||||||
//Store the page id in a var tag in case we need to access it with JavaScript.
|
|
||||||
|
|
||||||
$list = (isset($_REQUEST['list']) && !empty($_REQUEST['list'])) ? $_GET['list'] : "master";
|
|
||||||
//Get the value of "list" from the query key; default is "master" (the master list).
|
|
||||||
echo "<var id='query-list'>".$list."</var>";
|
|
||||||
//Store the list id in a var tag so that we can easily figure out what list to generate with list.js.
|
|
||||||
|
|
||||||
if($query_page=="lists"){
|
|
||||||
$lists_json = json_decode(file_get_contents(__DIR__ ."/../data/lists.json"));
|
|
||||||
echo "<script id='lists-json'>var lists=".json_encode($lists_json)."</script>";
|
|
||||||
//Copy the data from lists.json and paste it into a script tag, saving it as a variable called `list` to be accessed by later JavaScript code (i.e. lists.js).
|
|
||||||
//I would have stored it in a var tag, but there was a weird bug with some of the <u> tags leaking out and making my title underlined, and this seemed to avoid that (and it's probably more efficient too).
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
//$("#main-header").css({"font-size":$("#main-header").height()+"px"})
|
|
||||||
2
scripts/vendor/jquery-3.6.0.min.js
vendored
2
scripts/vendor/jquery-3.6.0.min.js
vendored
File diff suppressed because one or more lines are too long
@ -8,6 +8,7 @@
|
|||||||
/*https://www.schemecolor.com/light-silver-gradient.php)*/
|
/*https://www.schemecolor.com/light-silver-gradient.php)*/
|
||||||
--navy-blue:#091b75;
|
--navy-blue:#091b75;
|
||||||
--azure:#4f67db;
|
--azure:#4f67db;
|
||||||
|
--azure-tint-20: #6e87e5;
|
||||||
--charcoal:#333333;
|
--charcoal:#333333;
|
||||||
font-size:120%;
|
font-size:120%;
|
||||||
|
|
||||||
@ -22,47 +23,47 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Beleren;
|
font-family: Beleren;
|
||||||
src: url('fonts/Beleren-Bold.ttf');
|
src: url('/assets/fonts/Beleren-Bold.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Playbill;
|
font-family: Playbill;
|
||||||
src: url('fonts/Playbill.ttf');
|
src: url('/assets/fonts/Playbill.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Moderna;
|
font-family: Moderna;
|
||||||
src: url('fonts/MODERNA_.ttf');
|
src: url('/assets/fonts/MODERNA_.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Adventure;
|
font-family: Adventure;
|
||||||
src: url('fonts/Adventure.ttf');
|
src: url('/assets/fonts/Adventure.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Oxygen;
|
font-family: Oxygen;
|
||||||
src: url('fonts/OxygenMono-Regular.ttf');
|
src: url('/assets/fonts/OxygenMono-Regular.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Garamond;
|
font-family: Garamond;
|
||||||
src: url('fonts/EBGaramond.ttf');
|
src: url('/assets/fonts/EBGaramond.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Fira;
|
font-family: Fira;
|
||||||
src: url('fonts/FiraSans-Regular.ttf');
|
src: url('/assets/fonts/FiraSans-Regular.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: StitchWarrior;
|
font-family: StitchWarrior;
|
||||||
src: url('fonts/StitchWarrior demo.ttf');
|
src: url('/assets/fonts/StitchWarrior demo.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Floral;
|
font-family: Floral;
|
||||||
src: url('fonts/FloralCapitals.ttf');
|
src: url('/assets/fonts/FloralCapitals.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
var{
|
var{
|
||||||
@ -31,9 +31,12 @@ footer, header, hgroup, menu, nav, section {
|
|||||||
body {
|
body {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
ol, ul {
|
ul {
|
||||||
list-style: disc;
|
list-style: disc;
|
||||||
}
|
}
|
||||||
|
ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
blockquote, q {
|
blockquote, q {
|
||||||
quotes: none;
|
quotes: none;
|
||||||
}
|
}
|
||||||
@ -145,3 +145,63 @@ summary.heading{
|
|||||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[name="tag-selector"]{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-tag{
|
||||||
|
font-size: 0.6em;
|
||||||
|
padding: 0.1em;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:has(input[name="tag-selector"]){
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 0.1em;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
margin: 0.5em 0.25em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-tag, label:has(input[name="tag-selector"]){
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 3px 3px 3px 3px;
|
||||||
|
background-color: var(--azure);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
vertical-align: middle;
|
||||||
|
text-decoration: unset;
|
||||||
|
font-weight: unset;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-tag:hover, label:has(input[name="tag-selector"]):hover{
|
||||||
|
background-color: var(--azure-tint-20);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:has(input[name="tag-selector"]:checked){
|
||||||
|
background: var(--silver);
|
||||||
|
color: var(--charcoal);
|
||||||
|
}
|
||||||
|
|
||||||
|
article hr{
|
||||||
|
border-bottom: none;
|
||||||
|
border-color: var(--silver);
|
||||||
|
border-width: 0.1rem;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.rss-icon{
|
||||||
|
width: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
span img.rss-icon{
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
129
site/assets/img/favicon.svg
Normal file
129
site/assets/img/favicon.svg
Normal 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 |
18
site/assets/img/rss.svg
Normal file
18
site/assets/img/rss.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="128px" height="128px" id="RSSicon" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient x1="0.085" y1="0.085" x2="0.915" y2="0.915" id="RSSg">
|
||||||
|
<stop offset="0.0" stop-color="#E3702D"/><stop offset="0.1071" stop-color="#EA7D31"/>
|
||||||
|
<stop offset="0.3503" stop-color="#F69537"/><stop offset="0.5" stop-color="#FB9E3A"/>
|
||||||
|
<stop offset="0.7016" stop-color="#EA7C31"/><stop offset="0.8866" stop-color="#DE642B"/>
|
||||||
|
<stop offset="1.0" stop-color="#D95B29"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="55" ry="55" x="0" y="0" fill="#CC5D15"/>
|
||||||
|
<rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52"/>
|
||||||
|
<rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#RSSg)"/>
|
||||||
|
<circle cx="68" cy="189" r="24" fill="#FFF"/>
|
||||||
|
<path d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z" fill="#FFF"/>
|
||||||
|
<path d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z" fill="#FFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
11
site/assets/js/blog_archive_query.js
Normal file
11
site/assets/js/blog_archive_query.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
function selectTagByUrlQuery(){
|
||||||
|
query = new URLSearchParams(window.location.search);
|
||||||
|
tag = query.get('tag');
|
||||||
|
|
||||||
|
if(tag){
|
||||||
|
document.querySelector(`#tag-selector-${tag}`).checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('load', selectTagByUrlQuery);
|
||||||
|
selectTagByUrlQuery();
|
||||||
10
site/pages/index.md
Normal file
10
site/pages/index.md
Normal file
@ -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 <span title="'ʃɛpɪk">Shepich</span> (@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!
|
||||||
12
site/templates/components/blog_archive.html
Normal file
12
site/templates/components/blog_archive.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<article>
|
||||||
|
<h1 class="headline">{site.title} Archive</h1>
|
||||||
|
<hr />
|
||||||
|
<h2>Tag Inventory</h2>
|
||||||
|
{templates.components.blog_archive_tag_selector}
|
||||||
|
<br />
|
||||||
|
<h2>Post History</h2>
|
||||||
|
{content}
|
||||||
|
<br /><hr /><br />
|
||||||
|
<p>Last Updated: {globalvars.today}<span>{templates.components.rss_icon}</span></p>
|
||||||
|
</article>
|
||||||
|
<script src="/assets/js/blog_archive_query.js"></script>
|
||||||
1
site/templates/components/blog_archive_li.html
Normal file
1
site/templates/components/blog_archive_li.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<li>{article.metadata.date} - <a href='{article.path}'>{article.metadata.title}</a> {blog_tags}</li>
|
||||||
4
site/templates/components/blog_archive_tag_selector.html
Normal file
4
site/templates/components/blog_archive_tag_selector.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<form id="tag-selector">
|
||||||
|
{tag_selector_options}
|
||||||
|
</form>
|
||||||
|
{tag_selector_css_rules}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
<label for="tag-selector-{tag_name}">{tag_name} ({number_with_tag})
|
||||||
|
<input type="radio" name="tag-selector" value="{tag_name}" id="tag-selector-{tag_name}">
|
||||||
|
</label>
|
||||||
11
site/templates/components/blog_article.html
Normal file
11
site/templates/components/blog_article.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<article>
|
||||||
|
<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>
|
||||||
|
<br/>First published: <time pubdate datetime="{article.metadata.date}">{article.metadata.date}</time>
|
||||||
|
<br/>Last modified: <time pubdate datetime="{article.metadata.lastmod}">{article.metadata.lastmod}</time>
|
||||||
|
</p>
|
||||||
|
{article.content}
|
||||||
|
<br /><hr /><br />
|
||||||
|
<p>Tags: {blog_tags}<span>{templates.components.rss_icon}</span></p>
|
||||||
|
</article>
|
||||||
1
site/templates/components/blog_tag.html
Normal file
1
site/templates/components/blog_tag.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<a class='blog-tag' data='{tag_name}'' href='{site.base_url}/archive.html?tag={tag_name}'>{tag_name}</a>
|
||||||
11
site/templates/components/blog_tag_reference.html
Normal file
11
site/templates/components/blog_tag_reference.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<article>
|
||||||
|
<h1 class="headline">{site.title} Tag Reference</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="{article.metadata.date}">{article.metadata.date}</time>
|
||||||
|
<br/>Last modified: <time pubdate datetime="{article.metadata.lastmod}">{article.metadata.lastmod}</time>
|
||||||
|
</p>
|
||||||
|
{article.content}
|
||||||
|
<br /><hr /><br />
|
||||||
|
<p>{blog_tags}</p>
|
||||||
|
</article>
|
||||||
1
site/templates/components/rss_icon.html
Normal file
1
site/templates/components/rss_icon.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<a href="{site.base_url}/rss.xml"><img class='rss-icon' title='{site.title} RSS Feed' src='/assets/img/rss.svg' /></a>
|
||||||
5
site/templates/components/simple_article.html
Normal file
5
site/templates/components/simple_article.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<article>
|
||||||
|
<h1 class="headline">{article.metadata.title}</h1>
|
||||||
|
<hr />
|
||||||
|
{article.content}
|
||||||
|
</article>
|
||||||
18
site/templates/pages/default.html
Normal file
18
site/templates/pages/default.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="description" content="{site.description}">
|
||||||
|
<head>
|
||||||
|
{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>
|
||||||
|
{content}
|
||||||
|
</main>
|
||||||
|
{templates.partials.footer}
|
||||||
|
</body>
|
||||||
4
site/templates/partials/default_css.html
Normal file
4
site/templates/partials/default_css.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<link rel="stylesheet" type="text/css" href="/assets/css/reset.css">
|
||||||
|
<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">
|
||||||
3
site/templates/partials/footer.html
Normal file
3
site/templates/partials/footer.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<footer>
|
||||||
|
<br /><span class='copyright'>Copyright © 2021-{globalvars.today.year} Jim Shepich</span>
|
||||||
|
</footer>
|
||||||
3
site/templates/partials/header.html
Normal file
3
site/templates/partials/header.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<header id="main-header" class="no-highlight">
|
||||||
|
<span class="silver-text">Jimlab</span>
|
||||||
|
</header>
|
||||||
5
site/templates/partials/nav.html
Normal file
5
site/templates/partials/nav.html
Normal file
@ -0,0 +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/archive.html' target='_self'><div class='nav-tab'><span class='nav-text'>Dogma Jimfinium</span></div></a>
|
||||||
|
</nav>
|
||||||
222
testbench.ipynb
Normal file
222
testbench.ipynb
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"id": "207d2510",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import os\n",
|
||||||
|
"import re\n",
|
||||||
|
"import shutil\n",
|
||||||
|
"import markdown\n",
|
||||||
|
"import yaml\n",
|
||||||
|
"import subprocess\n",
|
||||||
|
"import rfeed\n",
|
||||||
|
"import pydantic\n",
|
||||||
|
"import glob\n",
|
||||||
|
"from dotmap import DotMap\n",
|
||||||
|
"from typing import Optional, Union, Literal, BinaryIO, Any\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"from datetime import datetime\n",
|
||||||
|
"from jimsite import *"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "68b107f1",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"id": "8f435a12",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"with open('config.yaml', 'r') as config_file:\n",
|
||||||
|
" config = yaml.safe_load(config_file.read())\n",
|
||||||
|
" \n",
|
||||||
|
"templates = map_templates(config['templates_folder'])\n",
|
||||||
|
"\n",
|
||||||
|
"sites = {k:SiteConfig(**v) for k,v in config['sites'].items()}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "e32458c7",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||||
|
"<rss version=\"2.0\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\"><channel><title>Dogma Jimfinium</title><link>http://localhost:8000/dogma-jimfinium/rss</link><description>Dogma Jimfinium</description><language>en-US</language><lastBuildDate>Thu, 29 Jan 2026 16:29:57 GMT</lastBuildDate><generator>rfeed v1.1.1</generator><docs>https://github.com/svpino/rfeed/blob/master/README.md</docs><item><title>Superlock</title><link>http://localhost:8000/dogma-jimfinium/superlock</link><author>Jim Shepich III</author><pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate><guid isPermaLink=\"true\">superlock</guid></item><item><title>Sustainable Living</title><link>http://localhost:8000/dogma-jimfinium/sustainable-living</link><author>Jim Shepich III</author><pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate><guid isPermaLink=\"true\">sustainable-living</guid></item><item><title>Stocking Up</title><link>http://localhost:8000/dogma-jimfinium/stocking-up</link><author>Jim Shepich III</author><pubDate>Wed, 19 Nov 2025 00:00:00 GMT</pubDate><guid isPermaLink=\"true\">stocking-up</guid></item><item><title>Set Up the Toys</title><link>http://localhost:8000/dogma-jimfinium/set-up-the-toys</link><author>Jim Shepich III</author><pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate><guid isPermaLink=\"true\">set-up-the-toys</guid></item><item><title>Do What You Love</title><link>http://localhost:8000/dogma-jimfinium/do-what-you-love</link><author>Jim Shepich III</author><pubDate>Tue, 10 Jun 2025 00:00:00 GMT</pubDate><guid isPermaLink=\"true\">do-what-you-love</guid></item><item><title>Self-Care is not Selfish</title><link>http://localhost:8000/dogma-jimfinium/self-care-is-not-selfish</link><author>Jim Shepich III</author><pubDate>Sun, 18 May 2025 00:00:00 GMT</pubDate><guid isPermaLink=\"true\">self-care-is-not-selfish</guid></item><item><title>Blowouts</title><link>http://localhost:8000/dogma-jimfinium/blowouts</link><author>Jim Shepich III</author><pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate><guid isPermaLink=\"true\">blowouts</guid></item><item><title>Vitamins & Supplements</title><link>http://localhost:8000/dogma-jimfinium/vitamins</link><author>Jim Shepich III</author><pubDate>Sun, 18 May 2025 00:00:00 GMT</pubDate><guid isPermaLink=\"true\">vitamins</guid></item><item><title>Gear for New Parents</title><link>http://localhost:8000/dogma-jimfinium/gear-for-new-parents</link><author>Jim Shepich III</author><pubDate>Fri, 12 Jul 2024 00:00:00 GMT</pubDate><guid isPermaLink=\"true\">gear-for-new-parents</guid></item></channel></rss>\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "70408b85",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "7de0d84d",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"index = {}\n",
|
||||||
|
"\n",
|
||||||
|
"for article in glob.glob('build/dogma-jimfinium/*.md'):\n",
|
||||||
|
" metadata, content = load_markdown(article)\n",
|
||||||
|
"\n",
|
||||||
|
" if metadata is None:\n",
|
||||||
|
" print(article)\n",
|
||||||
|
"\n",
|
||||||
|
" # Skip unpublished articles.\n",
|
||||||
|
" if not metadata.published:\n",
|
||||||
|
" continue\n",
|
||||||
|
"\n",
|
||||||
|
" article_filestem = os.path.splitext(os.path.basename(article))[0]\n",
|
||||||
|
"\n",
|
||||||
|
" # Add the article to the index.\n",
|
||||||
|
" index[article_filestem] = (metadata, content)\n",
|
||||||
|
"\n",
|
||||||
|
" # Interpolate the article contents into the webpage template.\n",
|
||||||
|
" article_html = format_html_template(\n",
|
||||||
|
" 'templates/components/blog_article.html',\n",
|
||||||
|
" content = content,\n",
|
||||||
|
" blog_tags = ' '.join(format_article_tags(metadata.tags)),\n",
|
||||||
|
" metadata = metadata\n",
|
||||||
|
" )\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, **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": "e3171afd",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 4,
|
||||||
|
"id": "a28b95a6",
|
||||||
|
"metadata": {},
|
||||||
|
"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": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": ".venv",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.12.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user