Created
August 26, 2024 03:14
-
-
Save cgranade/65d9b4f86c56ddbe5f5009215d5715e5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env -S uv run | |
# /// script | |
# requires-python = ">=3.12" | |
# dependencies = ["htpy", "click"] | |
# /// | |
""" | |
This is a terrible tool, make your peace with that first and foremost. | |
Still here? OK, this tool reads all gemtext files (*.gmi) in a given | |
directory, makes HTML out of them by using a terrible template, then | |
saves those files out to a different folder. | |
The HTML template is terrible because: | |
- It's hardcoded in. | |
- All CSS is in <head> instead of in factored out into stylesheets | |
that can be downloaded and cached. | |
- Well, except for the one CSS file whose location is hardcoded. Oops. | |
TODO: | |
- Make this tool part of a gitea-runner workflow that publishes gemtext | |
""" | |
import click | |
from htpy import html, head, body, style, link, h1, h2, h3, p, a, tt, div | |
from markupsafe import Markup | |
import pathlib | |
import glob | |
import re | |
def template(contents): | |
return html[ | |
head[ | |
link(rel="shortcut icon", href="https://linkshells.net/linkshells.png"), | |
link(rel="stylesheet", href="https://linkshells.net/fonts/reactor7/webfont.css"), | |
style[Markup(""" | |
:root { | |
--status-color: #be10c4; | |
--mp-color: #45d69a; | |
--hp-color: #205aa3; | |
--lv-color: #15dedf; | |
--drk-txt-color: #696a69; | |
--summon-materia-light:#fe5a5a; | |
--summon-materia-dark: #210000; | |
--magic-materia-light: #5bb65b; | |
--magic-materia-dark: #2d442d; | |
--command-materia-light: #e4e44d; | |
--command-materia-dark: #44440e; | |
--support-materia-light: #9191e4; | |
--support-materia-dark: #2e2e44; | |
--independent-materia-light: #de58de; | |
--independent-materia-dark: #2e0f2e; | |
} | |
body { | |
font-family: "Reactor7"; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
margin-top: 2em; | |
color: white; | |
background-color: black; | |
font-size: 24pt; | |
text-shadow: 2px 2px 4px black; | |
} | |
h1 { | |
font-size: 24pt; | |
padding: 0.4em; | |
margin-left: -2em; | |
width: 60%; | |
margin-top: 0em; | |
margin-bottom: 0.5em; | |
} | |
h2 { | |
font-size: 24pt; | |
color: var(--lv-color); | |
} | |
.panel, h1 { | |
background: linear-gradient(-45deg, rgb(0, 0, 32), rgb(0, 0, 176)); | |
border: 2px solid black; | |
border-radius: 0.3em; | |
} | |
.panel { padding: 1em; max-width: 80%; } | |
.panel table th { | |
font-size: 14pt; | |
} | |
.panel, h1 { | |
/* using values from https://www.finalfantasyforums.net/threads/default-window-colors.30391/ */ | |
border-radius: 0.3em; | |
border: 3px solid white; | |
box-shadow: 2px 2px black; | |
} | |
a { | |
margin-left: 1em; | |
font-weight: bold; | |
color: var(--mp-color); | |
text-decoration: none; | |
text-shadow: 2px 2px 4px #444; | |
} | |
@keyframes blinker { | |
50% { | |
opacity: 0; | |
} | |
} | |
a:visited { | |
color: var(--status-color); | |
text-decoration: none; | |
} | |
a:focus::before, a:hover::before { | |
position: fixed; | |
transform: translate(-56px, 3px); | |
content: url("data:;base64,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"); | |
} | |
a:hover::before { | |
animation: blinker 1s linear infinite; | |
position: fixed; | |
} | |
a:focus { | |
outline: none; | |
} | |
th { | |
color: var(--status-color) | |
} | |
""")] | |
], | |
body[div(".panel")[ | |
*contents | |
]] | |
] | |
def parse_to_html(gemtext): | |
# Gemtext has the nice property that you only need to look at the start | |
# of each line to figure out what kind of line it is. | |
accumulator = "" | |
for line in gemtext.split("\n"): | |
if line.startswith("###"): | |
if accumulator: | |
yield p[accumulator] | |
accumulator = "" | |
yield h3[line.removeprefix("###").strip()] | |
elif line.startswith("##"): | |
if accumulator: | |
yield p[accumulator] | |
accumulator = "" | |
yield h2[line.removeprefix("##").strip()] | |
elif line.startswith("#"): | |
if accumulator: | |
yield p[accumulator] | |
accumulator = "" | |
yield h1[line.removeprefix("#").strip()] | |
elif line.startswith("=>"): | |
if accumulator: | |
yield p[accumulator] | |
accumulator = "" | |
parts = line.removeprefix("=>").strip().split(" ", 1) | |
href = re.sub("\\.gmi$", ".html", parts[0]) | |
text = parts[1].strip() if len(parts) > 1 else tt[href] | |
yield p[a(href=href)[text]] | |
elif not line.strip(): | |
if accumulator: | |
yield p[accumulator] | |
accumulator = "" | |
# TODO: images and ``` blocks | |
else: | |
accumulator = accumulator + "\n" + line | |
if accumulator.strip(): | |
yield p[accumulator] | |
@click.command() | |
@click.argument("source", type=click.Path(file_okay=False, dir_okay=True)) | |
@click.argument("dest", type=click.Path(file_okay=False, dir_okay=True)) | |
def main(source, dest): | |
source = pathlib.Path(source) | |
dest = pathlib.Path(dest) | |
for gemfile in glob.glob("**/*.gmi", root_dir=source, recursive=True): | |
with open(source / gemfile) as f: | |
contents = template(parse_to_html(f.read())) | |
(dest / gemfile).parent.mkdir(parents=True, exist_ok=True) | |
with open((dest / gemfile).with_suffix(".html"), "w") as f: | |
f.write(str(contents)) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment