feat: Make good art (with Typst)
This commit is contained in:
parent
2ec156422b
commit
64a45a8641
10 changed files with 27414 additions and 27 deletions
|
|
@ -18,9 +18,13 @@
|
||||||
sync.allExtras = true;
|
sync.allExtras = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
typst = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
packages = [
|
packages = [
|
||||||
|
pkgs.hanken-grotesk
|
||||||
pkgs.uv
|
pkgs.uv
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
@ -17,8 +18,9 @@ MOBILIZON_API_URL = "https://events.hs3.pl/api"
|
||||||
QUERY_LIMIT = 100
|
QUERY_LIMIT = 100
|
||||||
TIME_WINDOW_DAYS = 365
|
TIME_WINDOW_DAYS = 365
|
||||||
TRUNCATE_AFTER_CHARACTERS = 300
|
TRUNCATE_AFTER_CHARACTERS = 300
|
||||||
TEMPLATE_FILENAME = "templates/newsletter_template.html"
|
TEMPLATES = {
|
||||||
OUTPUT_FILENAME = "newsletter_events.html"
|
"templates/newsletter_template.html": "newsletter_events.html",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def sanitize_html(raw_html: str) -> str:
|
def sanitize_html(raw_html: str) -> str:
|
||||||
|
|
@ -50,6 +52,7 @@ def build_graphql_query():
|
||||||
title
|
title
|
||||||
description
|
description
|
||||||
beginsOn
|
beginsOn
|
||||||
|
endsOn
|
||||||
picture {
|
picture {
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +94,7 @@ def fetch_events(begins_on: str, ends_on: str, limit: int = QUERY_LIMIT):
|
||||||
|
|
||||||
|
|
||||||
def prepare_events_for_template(raw_events: list) -> list:
|
def prepare_events_for_template(raw_events: list) -> list:
|
||||||
jours_semaine = [
|
days_week = [
|
||||||
"poniedziałek",
|
"poniedziałek",
|
||||||
"wtorek",
|
"wtorek",
|
||||||
"środa",
|
"środa",
|
||||||
|
|
@ -100,7 +103,7 @@ def prepare_events_for_template(raw_events: list) -> list:
|
||||||
"sobota",
|
"sobota",
|
||||||
"niedziela",
|
"niedziela",
|
||||||
]
|
]
|
||||||
mois_annee = [
|
month_year = [
|
||||||
"",
|
"",
|
||||||
"stycznia",
|
"stycznia",
|
||||||
"lutego",
|
"lutego",
|
||||||
|
|
@ -132,6 +135,7 @@ def prepare_events_for_template(raw_events: list) -> list:
|
||||||
begins_iso = ev.get("beginsOn")
|
begins_iso = ev.get("beginsOn")
|
||||||
picture = ev.get("picture")
|
picture = ev.get("picture")
|
||||||
picture_url = picture.get("url") if picture else None
|
picture_url = picture.get("url") if picture else None
|
||||||
|
picture_filename = ev.get("picture_filename")
|
||||||
link = ev.get("url") or ""
|
link = ev.get("url") or ""
|
||||||
|
|
||||||
if picture_url and "?" in picture_url:
|
if picture_url and "?" in picture_url:
|
||||||
|
|
@ -146,15 +150,15 @@ def prepare_events_for_template(raw_events: list) -> list:
|
||||||
location = ""
|
location = ""
|
||||||
|
|
||||||
dt_utc = datetime.datetime.fromisoformat(begins_iso.replace("Z", "+00:00"))
|
dt_utc = datetime.datetime.fromisoformat(begins_iso.replace("Z", "+00:00"))
|
||||||
dt_paris = dt_utc.astimezone(ZoneInfo("Europe/Paris"))
|
dt_warsaw = dt_utc.astimezone(ZoneInfo("Europe/Warsaw"))
|
||||||
jour_nom = jours_semaine[dt_paris.weekday()]
|
day_name = days_week[dt_warsaw.weekday()]
|
||||||
jour_num = dt_paris.day
|
day_num = dt_warsaw.day
|
||||||
mois_nom = mois_annee[dt_paris.month]
|
month_name = month_year[dt_warsaw.month]
|
||||||
annee = dt_paris.year
|
year = dt_warsaw.year
|
||||||
heure = dt_paris.hour
|
hour = dt_warsaw.hour
|
||||||
minute = dt_paris.minute
|
minute = dt_warsaw.minute
|
||||||
minute_str = f"{minute:02d}"
|
minute_str = f"{minute:02d}"
|
||||||
full_date = f"{jour_nom}, {jour_num} {mois_nom} {annee} @ {heure}:{minute_str}"
|
full_date = f"{day_name}, {day_num} {month_name} {year} @ {hour}:{minute_str}"
|
||||||
|
|
||||||
prepared.append(
|
prepared.append(
|
||||||
{
|
{
|
||||||
|
|
@ -162,6 +166,7 @@ def prepare_events_for_template(raw_events: list) -> list:
|
||||||
"description": truncated,
|
"description": truncated,
|
||||||
"full_date": full_date,
|
"full_date": full_date,
|
||||||
"picture_url": picture_url,
|
"picture_url": picture_url,
|
||||||
|
"picture_filename": picture_filename,
|
||||||
"location": location,
|
"location": location,
|
||||||
"link": link,
|
"link": link,
|
||||||
"start_time": dt_utc,
|
"start_time": dt_utc,
|
||||||
|
|
@ -191,6 +196,37 @@ def inline_css(input_html_path, output_html_path):
|
||||||
f.write(inlined_html)
|
f.write(inlined_html)
|
||||||
|
|
||||||
|
|
||||||
|
def download_picture():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def save_json_content(events):
|
||||||
|
json_dirname = "src/typst/content"
|
||||||
|
os.makedirs(json_dirname, exist_ok=True)
|
||||||
|
with open(os.path.join(json_dirname, "events.json"), "w") as f:
|
||||||
|
json.dump(sorted(events, key=lambda event: event["beginsOn"]), f)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_picture(event):
|
||||||
|
picture = event.get("picture")
|
||||||
|
picture_url = picture.get("url") if picture else None
|
||||||
|
picture_filename = None
|
||||||
|
|
||||||
|
if picture_url:
|
||||||
|
if "?" in picture_url:
|
||||||
|
picture_url = picture_url.split("?", 1)[0]
|
||||||
|
|
||||||
|
picture_dirname = "src/typst/assets/tmp"
|
||||||
|
picture_filename = os.path.join(picture_dirname, picture_url.split("/")[-1])
|
||||||
|
event["picture_filename"] = picture_filename
|
||||||
|
|
||||||
|
response = requests.get(picture_url)
|
||||||
|
|
||||||
|
os.makedirs(picture_dirname, exist_ok=True)
|
||||||
|
with open(picture_filename, "wb") as picture_file:
|
||||||
|
picture_file.write(response.content)
|
||||||
|
|
||||||
|
|
||||||
def _main():
|
def _main():
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
@ -218,21 +254,37 @@ def _main():
|
||||||
for ev in raw_events:
|
for ev in raw_events:
|
||||||
print(f"{ev.get('title', 'Untitled')} — {ev.get('beginsOn', 'Unknown date')}")
|
print(f"{ev.get('title', 'Untitled')} — {ev.get('beginsOn', 'Unknown date')}")
|
||||||
|
|
||||||
|
ev["txt_description"] = BeautifulSoup(ev["description"], "html.parser").get_text()
|
||||||
|
|
||||||
|
begins = ev["beginsOn"].split("T")
|
||||||
|
ends = ev["endsOn"].split("T")
|
||||||
|
txt_date = f"{begins[0]} {begins[1][:-4]}-"
|
||||||
|
if begins[0] != ends[0]:
|
||||||
|
txt_date += f"{ends[0]}"
|
||||||
|
txt_date += f"{ends[1][:-4]}"
|
||||||
|
ev["txt_date"] = txt_date
|
||||||
|
|
||||||
|
extract_picture(ev)
|
||||||
|
|
||||||
|
save_json_content(raw_events)
|
||||||
|
|
||||||
raw_events.sort(key=lambda ev: ev.get("beginsOn", ""))
|
raw_events.sort(key=lambda ev: ev.get("beginsOn", ""))
|
||||||
|
|
||||||
events = prepare_events_for_template(raw_events)
|
events = prepare_events_for_template(raw_events)
|
||||||
|
|
||||||
os.makedirs("dist", exist_ok=True)
|
for template_filename, output_filename in TEMPLATES.items():
|
||||||
output_path = os.path.join(os.getcwd(), "dist", OUTPUT_FILENAME)
|
output = render_newsletter(
|
||||||
html_output = render_newsletter(
|
events=events, template_dir=os.getcwd(), template_name=template_filename
|
||||||
events=events, template_dir=os.getcwd(), template_name=TEMPLATE_FILENAME
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
os.makedirs("dist", exist_ok=True)
|
||||||
|
output_path = os.path.join(os.getcwd(), "dist", output_filename)
|
||||||
with open(output_path, "w", encoding="utf-8") as f:
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
f.write(html_output)
|
f.write(output)
|
||||||
print(f"File '{output_path}' generated successfully.", file=sys.stderr)
|
print(f"File '{output_path}' generated successfully.", file=sys.stderr)
|
||||||
|
|
||||||
inlined_output_path = os.path.join(os.getcwd(), "dist", "newsletter_events_inlined.html")
|
if output_filename.endswith(".html"):
|
||||||
|
inlined_output_path = os.path.join(os.getcwd(), "dist", f"inlined_{output_filename}")
|
||||||
inline_css(output_path, inlined_output_path)
|
inline_css(output_path, inlined_output_path)
|
||||||
print(
|
print(
|
||||||
f"File '{inlined_output_path}' (inline CSS) generated.",
|
f"File '{inlined_output_path}' (inline CSS) generated.",
|
||||||
|
|
|
||||||
2
src/typst/.gitignore
vendored
Normal file
2
src/typst/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
assets/tmp/*
|
||||||
|
*.pdf
|
||||||
25941
src/typst/assets/calendar.svg
Normal file
25941
src/typst/assets/calendar.svg
Normal file
File diff suppressed because it is too large
Load diff
|
After Width: | Height: | Size: 1.4 MiB |
1244
src/typst/assets/event.svg
Normal file
1244
src/typst/assets/event.svg
Normal file
File diff suppressed because it is too large
Load diff
|
After Width: | Height: | Size: 68 KiB |
BIN
src/typst/assets/logo.png
Normal file
BIN
src/typst/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/typst/assets/photo.jpg
Normal file
BIN
src/typst/assets/photo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
69
src/typst/calendar.typ
Normal file
69
src/typst/calendar.typ
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#set page(
|
||||||
|
paper: "a4",
|
||||||
|
background: context {
|
||||||
|
place(center + horizon, {
|
||||||
|
image("assets/calendar.svg")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
margin: (top: .5cm, bottom: .5cm, x: 0cm),
|
||||||
|
)
|
||||||
|
|
||||||
|
#set text(fill: white)
|
||||||
|
|
||||||
|
#let pat = tiling(size: (30pt, 30pt))[
|
||||||
|
#place(line(start: (0%, 0%), end: (100%, 100%), stroke: white))
|
||||||
|
#place(line(start: (0%, 100%), end: (100%, 0%), stroke: white))
|
||||||
|
]
|
||||||
|
|
||||||
|
#let add_event(number, title, description, date, address) = {
|
||||||
|
let ex = 85mm + calc.rem-euclid(number, 2) * 67mm
|
||||||
|
let ey = 46mm + calc.div-euclid(number, 2) * 76mm
|
||||||
|
|
||||||
|
place(
|
||||||
|
top+left,
|
||||||
|
dx: ex,
|
||||||
|
dy: ey,
|
||||||
|
block(
|
||||||
|
width: 32mm,
|
||||||
|
height: 48mm,
|
||||||
|
|
||||||
|
[
|
||||||
|
#block(
|
||||||
|
height: 14mm,
|
||||||
|
clip: true,
|
||||||
|
text(
|
||||||
|
font: ("Hanken Grotesk"),
|
||||||
|
title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
#move(dy: 4mm,
|
||||||
|
block(
|
||||||
|
height: 3mm,
|
||||||
|
clip: true,
|
||||||
|
text(weight: "bold", size: 9pt, date)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
#move(dy: 11mm,
|
||||||
|
block(
|
||||||
|
height: 12mm,
|
||||||
|
clip: true,
|
||||||
|
text(size: 9pt, address)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let events = json("content/events.json")
|
||||||
|
#let index = 0
|
||||||
|
|
||||||
|
#for event in events {
|
||||||
|
add_event(index, event.title, event.txt_description, event.txt_date, "Hackerspace Trójmiasto, al. Wojska Polskiego 41, Gdańsk")
|
||||||
|
|
||||||
|
index = index + 1
|
||||||
|
|
||||||
|
if (index >= 6) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/typst/content/.gitkeep
Normal file
0
src/typst/content/.gitkeep
Normal file
75
src/typst/event.typ
Normal file
75
src/typst/event.typ
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
#import "@preview/bulb:0.1.0": dither
|
||||||
|
|
||||||
|
#set page(
|
||||||
|
width: 297mm,
|
||||||
|
height: 169mm,
|
||||||
|
background: context {
|
||||||
|
place(center + horizon, {
|
||||||
|
image("assets/event.svg")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
margin: (top: 0cm, bottom: .5cm, x: .5cm),
|
||||||
|
)
|
||||||
|
|
||||||
|
#let make_event_card(title, description, date, address, picture, name) = {
|
||||||
|
move(
|
||||||
|
dx: 148mm,
|
||||||
|
dy: 23mm,
|
||||||
|
image(
|
||||||
|
dither(
|
||||||
|
read("assets/photo.jpg", encoding: none),
|
||||||
|
mode: "bw",
|
||||||
|
method: "bayer8x8",
|
||||||
|
size: 360,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
place(
|
||||||
|
top+left,
|
||||||
|
dx: 94mm,
|
||||||
|
dy: -4mm,
|
||||||
|
image("assets/hs3_logo_hires.png", height: 38mm, width: 38mm),
|
||||||
|
)
|
||||||
|
|
||||||
|
set text(font: ("Hanken Grotesk"), size: 14pt)
|
||||||
|
|
||||||
|
place(
|
||||||
|
top+left,
|
||||||
|
dx: 12mm,
|
||||||
|
dy: 30mm,
|
||||||
|
block(
|
||||||
|
width: 118mm,
|
||||||
|
height: 130mm,
|
||||||
|
grid(
|
||||||
|
rows: (2fr, 6fr, 0.75fr, 0.5fr),
|
||||||
|
text(size: 28pt, weight: "bold", title),
|
||||||
|
text(size: 10pt, description),
|
||||||
|
text(
|
||||||
|
size: 24pt,
|
||||||
|
weight: "bold",
|
||||||
|
grid(
|
||||||
|
columns: (1fr, 1fr),
|
||||||
|
date.split(" ").at(0),
|
||||||
|
align(right)[#date.split(" ").at(1)]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
address,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
align(right+bottom)[#text(size: 14pt, weight: "bold", name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
#let events = json("content/events.json")
|
||||||
|
#let index = 0
|
||||||
|
|
||||||
|
#for event in events {
|
||||||
|
make_event_card(event.title, event.txt_description, event.txt_date, "Gdańsk, al. Wojska Polskiego 41", "", "Joanna Roscoe")
|
||||||
|
|
||||||
|
index = index + 1
|
||||||
|
|
||||||
|
if index < events.len() {
|
||||||
|
pagebreak()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue