348 lines
15 KiB
Python
348 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""Generate the expanded, rebranded static site from the Chadnet mirror + curated additions.
|
||
Every page is written as *.html and every internal page-link points to the .html file,
|
||
so any static host renders it instead of offering a download."""
|
||
import json, os, re, shutil, html as H
|
||
import content_extra as CE
|
||
|
||
SITE_NAME = "Library of GigaChad"
|
||
SITE_SHORT = "GigaChad"
|
||
BLOB = "Become a GigaChad."
|
||
MIRROR = "/home/hermes/chad-library/mirror"
|
||
SITED = "/home/hermes/chad-library/site"
|
||
|
||
TOP = [
|
||
("arts-and-literature", "Arts & Literature", "ships.jpg"),
|
||
("computers-and-internet", "Computers & Internet", "matrix.png"),
|
||
("health", "Health", "pietro.png"),
|
||
("history", "History", "rome2.jpg"),
|
||
("politics-philosophy-and-society","Politics, Philosophy & Society","putin.jpg"),
|
||
("practical-skills", "Practical Skills", "forest.jpg"),
|
||
("religion-and-spirituality", "Religion & Spirituality", "religion.jpg"),
|
||
("science", "Science", "cocaine.jpg"),
|
||
]
|
||
NEW_TOP = [
|
||
("3d-printing-and-fabrication", "3D Printing & Fabrication", "matrix.png"),
|
||
("physical-culture", "Physical Culture", "putin.jpg"),
|
||
("homesteading-and-off-grid", "Homesteading & Off-Grid", "forest.jpg"),
|
||
("engineering-and-radio", "Engineering & Radio", "matrix.png"),
|
||
("other", "Other (WIP)", "xp.jpg"),
|
||
]
|
||
ALL_TOP = TOP + NEW_TOP
|
||
NEW_SET = {"3d-printing-and-fabrication","physical-culture","homesteading-and-off-grid","engineering-and-radio"}
|
||
TITLE_BY_SLUG = {s: t for s, t, _ in ALL_TOP}
|
||
BG_BY_SLUG = {s: "/icons/home/" + b for s, _, b in ALL_TOP}
|
||
NEW_CATEGORY_OBJS = [CE.NEW_CATEGORY_3DPRINT, CE.NEW_CATEGORY_FITNESS,
|
||
CE.NEW_CATEGORY_HOMESTEAD, CE.NEW_CATEGORY_ELECTRONICS]
|
||
|
||
ICON = {
|
||
"folder": "/icons/links/folder.png",
|
||
"globe": "/icons/links/globe.svg",
|
||
"document": "/icons/links/document.png",
|
||
"painting": "/icons/links/painting.png",
|
||
"youtube": "/icons/links/youtube.svg",
|
||
"article": "/icons/links/article.png",
|
||
"chadsquare": "/icons/links/chad-square.png",
|
||
"link": "/icons/links/book.png",
|
||
}
|
||
|
||
def esc(t): return H.escape(str(t))
|
||
def page(slug): return slug + ".html"
|
||
|
||
# ---------------------------------------------------------------- link fixing
|
||
def fix_href(val):
|
||
"""Rewrite a single (relative) href/src to the flat *.html scheme.
|
||
External, fragments, anchors and file-assets stay; internal page links get .html."""
|
||
val = val.strip()
|
||
if not val:
|
||
return val
|
||
if val.startswith(("http", "//", "mailto", "tel", "data:", "javascript:", "ftp:")):
|
||
return val
|
||
frag = q = ""
|
||
if "#" in val:
|
||
val, frag = val.split("#", 1); frag = "#" + frag
|
||
if "?" in val:
|
||
val, q = val.split("?", 1); q = "?" + q
|
||
if not val:
|
||
return q + frag
|
||
absolute = val.startswith("/")
|
||
# strip leading dir prefixes; keep the real remaining subpath for files
|
||
segments = [x for x in val.split("/") if x not in ("", ".", "..")]
|
||
if not segments:
|
||
return ("/" if absolute else "") + q + frag
|
||
base = segments[-1]
|
||
if "." in base:
|
||
# file asset — keep its real subpath (files live under /files or root)
|
||
rel = "/".join(segments)
|
||
return "/" + rel + q + frag
|
||
# internal page — flatten to a root *.html
|
||
return "/" + base + ".html" + q + frag
|
||
|
||
def rewrite_html_links(content):
|
||
return re.sub(r'(href|src)=(["\'])([^"\']*)\2',
|
||
lambda m: m.group(1) + "=" + m.group(2) + fix_href(m.group(3)) + m.group(2),
|
||
content)
|
||
|
||
def rebrand(html):
|
||
html = html.replace("https://wiki.chadnet.org", "")
|
||
html = html.replace("https://lib.chadnet.org", "")
|
||
html = html.replace("Library of Chadnet", SITE_NAME)
|
||
html = html.replace("— Chadnet", "— " + SITE_SHORT)
|
||
html = html.replace("– Chadnet", "– " + SITE_SHORT)
|
||
html = html.replace('>wiki.chadnet.org<', '>' + SITE_SHORT + '<')
|
||
html = html.replace("Chadnet", SITE_SHORT)
|
||
return html
|
||
|
||
# ---------------------------------------------------------------- renderers
|
||
def head(title):
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>{esc(title)}</title>
|
||
<link rel="icon" href="/favicon.ico">
|
||
<link rel="stylesheet" href="/style.css?v=1">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:title" content="{esc(title)}">
|
||
<meta property="og:description" content="The greatest website on the interwebz!">
|
||
</head>
|
||
<body>
|
||
"""
|
||
|
||
def banner():
|
||
return '<div style="text-align: center;"><a href="/"><img class="epic" src="/icons/chads/chad.png"></a></div>'
|
||
|
||
def footer():
|
||
return f""" <hr>
|
||
<p class="C">{esc(SITE_NAME)} | <a href="/">{esc(SITE_SHORT)}</a></p>
|
||
</body>
|
||
</html>"""
|
||
|
||
def nav(parent_slug):
|
||
if not parent_slug:
|
||
back = '<a href="/">Home</a>'
|
||
else:
|
||
back = f'<a href="/{esc(page(parent_slug))}">{esc(TITLE_BY_SLUG.get(parent_slug, parent_slug))}</a>'
|
||
return f'<p class="nav"><span class="nav-left">Back: {back}</span><span class="nav-right"><a href="/theme">Theme</a></span></p>'
|
||
|
||
def render_item(it, extra=False):
|
||
href = it["href"]
|
||
text = it["text"] or ""
|
||
note = it.get("note", "")
|
||
itype = it["type"]
|
||
icon = it.get("icon") or ICON.get(itype, ICON["link"])
|
||
if not href:
|
||
a = esc(text)
|
||
elif href.startswith("http") or href.startswith("//"):
|
||
a = f'<a href="{esc(href)}" target="_blank">{esc(text)}</a>'
|
||
elif href.startswith("mailto"):
|
||
a = f'<a href="{esc(href)}">{esc(text)}</a>'
|
||
else:
|
||
a = f'<a href="{esc(fix_href(href))}">{esc(text)}</a>'
|
||
if note:
|
||
a += " " + note
|
||
return f' <li><img src="{esc(icon)}" draggable="false">{a}</li>'
|
||
|
||
def render_section(h2key, items, extra=False):
|
||
out = []
|
||
if h2key:
|
||
out.append(f'<h2>{esc(h2key)}</h2>')
|
||
out.append('<ul class="category">')
|
||
for it in items:
|
||
out.append(render_item(it, extra))
|
||
out.append('</ul>')
|
||
return "\n".join(out)
|
||
|
||
def _clean_title(t):
|
||
# strip any trailing "… <token> Chadnet" (dash may be mojibake), then any "— GigaChad"
|
||
t = re.sub(r"\s+\S+\s*Chadnet\s*$", "", t) # "… — Chadnet" footer from source
|
||
t = re.sub(r"\s*[–—-]\s*GigaChad\s*$", "", t)
|
||
return t.strip()
|
||
|
||
def category_page(slug):
|
||
d = json.load(open(os.path.join(MIRROR, "..", "site_data.json")))
|
||
pg = d["pages"].get(slug)
|
||
title = TITLE_BY_SLUG.get(slug, _clean_title(pg["title"]) if pg else slug)
|
||
parent = pg["parent"] if pg else None
|
||
sections = list(pg["sections"]) if pg else []
|
||
if slug in CE.EXPANSIONS:
|
||
sections = sections + CE.EXPANSIONS[slug]
|
||
out = [head(f"{title} — {SITE_SHORT}"), f' <h1>{esc(title)}</h1>',
|
||
banner(), ' <hr>', nav(parent)]
|
||
for h2key, items in sections:
|
||
out.append(render_section(h2key, items))
|
||
out.append(footer())
|
||
return "\n".join(out)
|
||
|
||
def new_category_page(cat):
|
||
title = cat["title"]
|
||
out = [head(f"{title} — {SITE_SHORT}"), f' <h1>{esc(title)}</h1>',
|
||
banner(), ' <hr>', nav(None)]
|
||
if cat.get("blurb"):
|
||
out.append(f'<p>{esc(cat["blurb"])}</p>')
|
||
for h2key, items in cat["sections"]:
|
||
out.append(render_section(h2key, items, extra=True))
|
||
out.append(footer())
|
||
return "\n".join(out)
|
||
|
||
def homepage():
|
||
tiles = []
|
||
for slug, title, _ in ALL_TOP:
|
||
newtag = ' <span style="font-size:14px;">(NEW)</span>' if slug in NEW_SET else ""
|
||
tiles.append(f'<a href="/{esc(page(slug))}"><div class="maincat">\n'
|
||
f' <div style="background-image: url(\'{BG_BY_SLUG[slug]}\');background-position:50%% 50%%;"></div>\n'
|
||
f' <h1>{esc(title)}{newtag}</h1>\n </div></a>\n <br>')
|
||
tiles_html = "\n".join(tiles)
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>{esc(SITE_NAME)}</title>
|
||
<link rel="icon" href="/favicon.ico">
|
||
<link rel="stylesheet" href="/style.css?v=1">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:title" content="{esc(SITE_NAME)}">
|
||
<meta property="og:description" content="The greatest website on the interwebz!">
|
||
</head>
|
||
<body>
|
||
<h1>
|
||
{esc(SITE_NAME)}
|
||
<br>
|
||
<small id="message"><noscript><em><q>{esc(BLOB)}</q></em></noscript></small>
|
||
</h1>
|
||
<script>setMsg();</script>
|
||
<div style="text-align: center;">
|
||
<div class="epic" style="display: inline-block;">
|
||
<a href="/"><img class="glow" src="/icons/chads/chad.png"></a>
|
||
</div>
|
||
</div>
|
||
<hr>
|
||
<p class="C"><a href="/about#how-do-i-contribute">Submit article</a> | <a href="/theme">Theme</a><br><a href="/donate"><b>Donate</b></a></p>
|
||
<p>Welcome to {esc(SITE_NAME)}—a library of information. <a href="/about">Moar info</a>.</p>
|
||
<p><strong>News:</strong>
|
||
Everything Chadnet ever had, plus all-new categories — 3D Printing & Fabrication, Physical Culture, Homesteading & Off-Grid, Engineering & Radio — and hundreds more resources.
|
||
</p>
|
||
{tiles_html}
|
||
<hr>
|
||
<p class="C">{esc(SITE_NAME)} | <a href="/">{esc(SITE_SHORT)}</a></p>
|
||
</body>
|
||
</html>"""
|
||
|
||
MSG = ["Because chad.net was taken.","A GigaChad owns his printer.","More power to the people.",
|
||
"Print it. Assemble it. Carry it.","Respect the machine, or the machine respects you.","Veni, vidi, vici.",
|
||
"Stronger every year.","Under the ice.","Become ungovernable.","Real men make things.","VICTORY TO THE GODS!",
|
||
"Many articools!","Fueled by curiosity.","TALLY-HO!","Wake up.","Remember who you are.","Grug loves filament.",
|
||
"A site for sore eyes!","Shop class will save America.","Self-reliance is the point.","Sun and steel.",
|
||
"A man prepares.","Build your own.","The library never closes.","Handmade.","Operated from the garage.",
|
||
"Yes hello.","Yarr!","Standing by.","Bring back the shop class.","Do it yourself, or find who does."]
|
||
|
||
def msg_js():
|
||
return "let messages = " + json.dumps(MSG) + ";\n" + (
|
||
"function setMsg() {\n const el = document.getElementById('message');\n"
|
||
" if (el) el.textContent = '« ' + messages[Math.floor(Math.random() * messages.length)] + ' »';\n}\n")
|
||
|
||
def simple_page(title, body):
|
||
o = head(title) + f' <h1>{esc(title)}</h1>\n' + banner() + '\n <hr>\n' + nav(None) + '\n'
|
||
for line in body.split("\n"):
|
||
line = line.strip()
|
||
if not line: continue
|
||
if line.startswith("###"): o += f"<h3>{esc(line[4:])}</h3>\n"
|
||
elif line.startswith("##"): o += f"<h2>{esc(line[3:])}</h2>\n"
|
||
elif line.startswith("```") and line == "```": o += "</pre>\n"
|
||
elif line.startswith("```"): o += "<pre>"
|
||
elif line.startswith("- "): o += f"<li>{esc(line[2:])}</li>\n"
|
||
else: o += f"<p>{esc(line)}</p>\n"
|
||
o += footer()
|
||
return o
|
||
|
||
# ----------------------------------------------------------------- main
|
||
def main():
|
||
shutil.rmtree(SITED, ignore_errors=True)
|
||
shutil.copytree(MIRROR, SITED)
|
||
for f in ["_progress.log"]:
|
||
p = os.path.join(SITED, f)
|
||
if os.path.exists(p): os.remove(p)
|
||
|
||
d = json.load(open(os.path.join(SITED, "..", "site_data.json")))
|
||
data_slugs = set(d["pages"].keys()) | set(TITLE_BY_SLUG)
|
||
|
||
# 1) rename extensionless pages -> .html ; rewrite links + rebrand every real page
|
||
renamed = {}
|
||
for root, _, files in os.walk(SITED):
|
||
for fn in files:
|
||
if "." in fn:
|
||
continue
|
||
full = os.path.join(root, fn)
|
||
try:
|
||
data = open(full, encoding="utf-8", errors="ignore").read()
|
||
except Exception:
|
||
continue
|
||
if "<html" not in data.lower() and "<body" not in data.lower():
|
||
continue
|
||
newfull = os.path.join(root, fn + ".html")
|
||
os.rename(full, newfull)
|
||
renamed.setdefault(os.path.relpath(root, SITED), []).append(fn)
|
||
|
||
# 2) every html page: rewrite internal links and rebrand
|
||
for root, _, files in os.walk(SITED):
|
||
for fn in files:
|
||
if not fn.endswith(".html"):
|
||
continue
|
||
full = os.path.join(root, fn)
|
||
try:
|
||
c = open(full, encoding="utf-8", errors="ignore").read()
|
||
except Exception:
|
||
continue
|
||
c = rewrite_html_links(c)
|
||
c = rebrand(c)
|
||
open(full, "w").write(c)
|
||
|
||
# 3) custom message.js
|
||
with open(os.path.join(SITED, "message.js"), "w") as f:
|
||
f.write(msg_js())
|
||
|
||
# 4) homepage
|
||
with open(os.path.join(SITED, "index.html"), "w") as f:
|
||
f.write(homepage())
|
||
|
||
# 5) regenerate category pages and new categories
|
||
for slug in list(data_slugs):
|
||
with open(os.path.join(SITED, page(slug)), "w") as f:
|
||
f.write(category_page(slug))
|
||
for cat in NEW_CATEGORY_OBJS:
|
||
with open(os.path.join(SITED, page(cat["slug"])), "w") as f:
|
||
f.write(new_category_page(cat))
|
||
|
||
# 6) about / donate / theme
|
||
about = ("## What is " + SITE_NAME + "?\n\n"
|
||
"It's a sort of mix between a web directory, archive, and compendium of knowledge — "
|
||
"everything the Library of Chadnet had, and more: new categories like 3D Printing & Fabrication, "
|
||
"Physical Culture, Homesteading & Off-Grid and Engineering & Radio, hundreds more resources, "
|
||
"same do-it-yourself spirit.\n\n"
|
||
"Not all information here may be true, and we are not responsible for anything you do with it.\n\n"
|
||
"## How do I contribute?\n\n"
|
||
"E-mail an image, article link, PDF, video link, etc. to the maintainer, or open a PR in the repository.\n\n"
|
||
"## How often is it updated?\n\n"
|
||
"On and off. Sometimes every few days, sometimes after a week or two.\n\n"
|
||
"## I own something here and want to take it down (copyrightz n stuff)\n\n"
|
||
"Prove you are the creator and ask to have it taken down. Everything here is free and we link back to the original authors.\n\n"
|
||
"## I wish to give you money\n\n"
|
||
"Yes, you can send money or crypto. See the donations page.")
|
||
donate = ("# Donate\n\nSupport the cause: keep the library free and growing.\n\n"
|
||
"- Bitcoin, Ethereum, Monero, or a coffee — details TBD by the maintainer.\n\n"
|
||
"Every contribution helps buy filament and server uptime.")
|
||
theme = ("# Theme\n\nThe theme uses CSS variables set on `:root`. Override from the browser console:\n\n"
|
||
"```js\nlocalStorage.setItem('--bg', 'darkblue')\nlocalStorage.setItem('--link', 'orange')\n```\n\n"
|
||
"Refresh to apply. `localStorage.clear()` resets.")
|
||
with open(os.path.join(SITED, "about.html"), "w") as f: f.write(simple_page("About", about))
|
||
with open(os.path.join(SITED, "donate.html"), "w") as f: f.write(simple_page("Donate", donate))
|
||
with open(os.path.join(SITED, "theme.html"), "w") as f: f.write(simple_page("Theme", theme))
|
||
|
||
print("Site generated at", SITED)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|