Merge pull request 'refactor' (#1) from refactor into main
Reviewed-on: jim/shepich.com#1
This commit is contained in:
commit
643c10303d
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)*/
|
||||
--navy-blue:#091b75;
|
||||
--azure:#4f67db;
|
||||
--azure-tint-20: #6e87e5;
|
||||
--charcoal:#333333;
|
||||
font-size:120%;
|
||||
|
||||
@ -22,47 +23,47 @@
|
||||
|
||||
@font-face {
|
||||
font-family: Beleren;
|
||||
src: url('fonts/Beleren-Bold.ttf');
|
||||
src: url('/assets/fonts/Beleren-Bold.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Playbill;
|
||||
src: url('fonts/Playbill.ttf');
|
||||
src: url('/assets/fonts/Playbill.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Moderna;
|
||||
src: url('fonts/MODERNA_.ttf');
|
||||
src: url('/assets/fonts/MODERNA_.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Adventure;
|
||||
src: url('fonts/Adventure.ttf');
|
||||
src: url('/assets/fonts/Adventure.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Oxygen;
|
||||
src: url('fonts/OxygenMono-Regular.ttf');
|
||||
src: url('/assets/fonts/OxygenMono-Regular.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Garamond;
|
||||
src: url('fonts/EBGaramond.ttf');
|
||||
src: url('/assets/fonts/EBGaramond.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Fira;
|
||||
src: url('fonts/FiraSans-Regular.ttf');
|
||||
src: url('/assets/fonts/FiraSans-Regular.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: StitchWarrior;
|
||||
src: url('fonts/StitchWarrior demo.ttf');
|
||||
src: url('/assets/fonts/StitchWarrior demo.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Floral;
|
||||
src: url('fonts/FloralCapitals.ttf');
|
||||
src: url('/assets/fonts/FloralCapitals.ttf');
|
||||
}
|
||||
|
||||
var{
|
||||
@ -31,9 +31,12 @@ footer, header, hgroup, menu, nav, section {
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
@ -145,3 +145,63 @@ summary.heading{
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
-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