Add havelseite
This commit is contained in:
BIN
generator/__pycache__/havelseiten.cpython-311.pyc
Normal file
BIN
generator/__pycache__/havelseiten.cpython-311.pyc
Normal file
Binary file not shown.
BIN
generator/__pycache__/havelseiten.cpython-38.pyc
Normal file
BIN
generator/__pycache__/havelseiten.cpython-38.pyc
Normal file
Binary file not shown.
BIN
generator/__pycache__/settings_config.cpython-311.pyc
Normal file
BIN
generator/__pycache__/settings_config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
generator/__pycache__/settings_config.cpython-38.pyc
Normal file
BIN
generator/__pycache__/settings_config.cpython-38.pyc
Normal file
Binary file not shown.
BIN
generator/__pycache__/settings_editor.cpython-311.pyc
Normal file
BIN
generator/__pycache__/settings_editor.cpython-311.pyc
Normal file
Binary file not shown.
BIN
generator/__pycache__/settings_editor.cpython-38.pyc
Normal file
BIN
generator/__pycache__/settings_editor.cpython-38.pyc
Normal file
Binary file not shown.
BIN
generator/__pycache__/validate.cpython-311.pyc
Normal file
BIN
generator/__pycache__/validate.cpython-311.pyc
Normal file
Binary file not shown.
BIN
generator/__pycache__/validate.cpython-38.pyc
Normal file
BIN
generator/__pycache__/validate.cpython-38.pyc
Normal file
Binary file not shown.
53
generator/components.json
Normal file
53
generator/components.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"hero": "hero",
|
||||
"aufmacher": "hero",
|
||||
|
||||
"text": "text",
|
||||
|
||||
"kacheln": "cards",
|
||||
"cards": "cards",
|
||||
|
||||
"two-columns": "two-columns",
|
||||
"zwei-spalten": "two-columns",
|
||||
|
||||
"gallery": "gallery",
|
||||
"galerie": "gallery",
|
||||
|
||||
"quote": "quote",
|
||||
"zitat": "quote",
|
||||
|
||||
"banner": "banner",
|
||||
"hinweis": "banner",
|
||||
|
||||
"timeline": "timeline",
|
||||
"zeitstrahl": "timeline",
|
||||
|
||||
"fragen": "faq",
|
||||
"faq": "faq",
|
||||
|
||||
"contact": "contact",
|
||||
"kontakt": "contact",
|
||||
|
||||
"location": "location",
|
||||
"ort": "location",
|
||||
"anfahrt": "location",
|
||||
|
||||
"image-text": "image-text",
|
||||
"bild-text": "image-text",
|
||||
|
||||
"event": "event",
|
||||
"veranstaltung": "event",
|
||||
"events": "events",
|
||||
"veranstaltungen": "events",
|
||||
|
||||
"dateien": "downloads",
|
||||
"downloads": "downloads",
|
||||
|
||||
"person": "person",
|
||||
|
||||
"personen": "people",
|
||||
"people": "people",
|
||||
|
||||
"sponsoren": "sponsors",
|
||||
"sponsors": "sponsors"
|
||||
}
|
||||
1692
generator/havelseiten.py
Normal file
1692
generator/havelseiten.py
Normal file
File diff suppressed because it is too large
Load Diff
32
generator/settings.json
Normal file
32
generator/settings.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"title": "Havelseiten",
|
||||
"language": "de",
|
||||
"validator_language": "de",
|
||||
"navigation": {
|
||||
"height": "large"
|
||||
},
|
||||
"logo": {
|
||||
"enabled": true,
|
||||
"placement": "header",
|
||||
"src": "msvb_logo.png",
|
||||
"alt": "Märkischer Seglerverein Beetzsee",
|
||||
"text": "Havelseiten"
|
||||
},
|
||||
"colors": {
|
||||
"background": "#ffffff",
|
||||
"text": "#111827",
|
||||
"link": "#0f766e",
|
||||
"link_hover": "#115e59",
|
||||
"surface": "#ffffff",
|
||||
"surface_alt": "#f1f5f9",
|
||||
"muted_text": "#4b5563",
|
||||
"border": "#dddddd",
|
||||
"header_background": "#111827",
|
||||
"header_hover": "#1f2937",
|
||||
"header_text": "#ffffff",
|
||||
"footer_background": "#111827",
|
||||
"footer_text": "#ffffff",
|
||||
"hero_background": "#f1f5f9",
|
||||
"accent": "#111827"
|
||||
}
|
||||
}
|
||||
1022
generator/settings_config.py
Normal file
1022
generator/settings_config.py
Normal file
File diff suppressed because it is too large
Load Diff
325
generator/static/external-content.js
Normal file
325
generator/static/external-content.js
Normal file
@ -0,0 +1,325 @@
|
||||
const CONSENT_PREFIX = "havelseiten:external:";
|
||||
|
||||
function storageGet(key) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function storageSet(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch (error) {
|
||||
// If storage is unavailable, the current in-page load still works.
|
||||
}
|
||||
}
|
||||
|
||||
function consentKey(button) {
|
||||
if (button.dataset.consentKey) {
|
||||
return `${CONSENT_PREFIX}${button.dataset.consentKey}`;
|
||||
}
|
||||
|
||||
if (button.dataset.mapAddress) {
|
||||
return `${CONSENT_PREFIX}map:${button.dataset.mapAddress}`;
|
||||
}
|
||||
|
||||
if (button.dataset.embedSrc) {
|
||||
return `${CONSENT_PREFIX}embed:${button.dataset.embedSrc}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function mapEmbedUrl(lat, lon) {
|
||||
const latitude = Number(lat);
|
||||
const longitude = Number(lon);
|
||||
const bbox = [
|
||||
longitude - 0.01,
|
||||
latitude - 0.006,
|
||||
longitude + 0.01,
|
||||
latitude + 0.006
|
||||
].join(",");
|
||||
|
||||
return (
|
||||
"https://www.openstreetmap.org/export/embed.html"
|
||||
+ `?bbox=${encodeURIComponent(bbox)}`
|
||||
+ "&layer=mapnik"
|
||||
+ `&marker=${encodeURIComponent(`${latitude},${longitude}`)}`
|
||||
);
|
||||
}
|
||||
|
||||
async function geocodeAddress(address) {
|
||||
const url = (
|
||||
"https://nominatim.openstreetmap.org/search"
|
||||
+ `?format=json&limit=1&q=${encodeURIComponent(address)}`
|
||||
);
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
const results = await response.json();
|
||||
|
||||
if (!results.length) {
|
||||
throw new Error("Adresse nicht gefunden");
|
||||
}
|
||||
|
||||
return results[0];
|
||||
}
|
||||
|
||||
async function loadEmbed(button, remember = true) {
|
||||
const iframe = document.createElement("iframe");
|
||||
const key = consentKey(button);
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = "Wird geladen...";
|
||||
|
||||
if (button.dataset.mapAddress) {
|
||||
const cachedMapUrl = storageGet(`${key}:src`);
|
||||
|
||||
if (cachedMapUrl) {
|
||||
iframe.src = cachedMapUrl;
|
||||
} else {
|
||||
try {
|
||||
const place = await geocodeAddress(button.dataset.mapAddress);
|
||||
iframe.src = mapEmbedUrl(place.lat, place.lon);
|
||||
storageSet(`${key}:src`, iframe.src);
|
||||
} catch (error) {
|
||||
button.disabled = false;
|
||||
button.textContent = "Karte konnte nicht geladen werden";
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
iframe.src = button.dataset.embedSrc;
|
||||
}
|
||||
|
||||
iframe.className = button.dataset.embedClass || "";
|
||||
iframe.title = button.dataset.embedTitle || "Externer Inhalt";
|
||||
iframe.loading = "lazy";
|
||||
iframe.allow = button.dataset.embedAllow || "";
|
||||
iframe.allowFullscreen = true;
|
||||
|
||||
if (remember && key) {
|
||||
storageSet(key, "1");
|
||||
}
|
||||
|
||||
button.closest(".external-placeholder, .location-placeholder").replaceWith(iframe);
|
||||
}
|
||||
|
||||
function galleryItems() {
|
||||
return [...document.querySelectorAll(".gallery-grid a")].map((link) => ({
|
||||
alt: link.querySelector("img")?.alt || "",
|
||||
height: link.dataset.pswpHeight,
|
||||
src: link.href,
|
||||
width: link.dataset.pswpWidth
|
||||
}));
|
||||
}
|
||||
|
||||
function createLightbox() {
|
||||
const lightbox = document.createElement("div");
|
||||
lightbox.className = "site-lightbox";
|
||||
lightbox.hidden = true;
|
||||
lightbox.innerHTML = `
|
||||
<button class="site-lightbox-close" type="button" aria-label="Schliessen">×</button>
|
||||
<button class="site-lightbox-prev" type="button" aria-label="Vorheriges Bild">‹</button>
|
||||
<figure>
|
||||
<img alt="">
|
||||
<figcaption></figcaption>
|
||||
</figure>
|
||||
<button class="site-lightbox-next" type="button" aria-label="Naechstes Bild">›</button>
|
||||
`;
|
||||
document.body.append(lightbox);
|
||||
|
||||
return lightbox;
|
||||
}
|
||||
|
||||
function initGallery() {
|
||||
const items = galleryItems();
|
||||
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lightbox = createLightbox();
|
||||
const image = lightbox.querySelector("img");
|
||||
const caption = lightbox.querySelector("figcaption");
|
||||
let currentIndex = 0;
|
||||
let touchStartX = 0;
|
||||
|
||||
function show(index) {
|
||||
currentIndex = (index + items.length) % items.length;
|
||||
const item = items[currentIndex];
|
||||
image.src = item.src;
|
||||
image.alt = item.alt;
|
||||
caption.textContent = item.alt;
|
||||
caption.hidden = !item.alt;
|
||||
lightbox.hidden = false;
|
||||
document.body.classList.add("lightbox-open");
|
||||
}
|
||||
|
||||
function close() {
|
||||
lightbox.hidden = true;
|
||||
image.removeAttribute("src");
|
||||
document.body.classList.remove("lightbox-open");
|
||||
}
|
||||
|
||||
function next() {
|
||||
show(currentIndex + 1);
|
||||
}
|
||||
|
||||
function prev() {
|
||||
show(currentIndex - 1);
|
||||
}
|
||||
|
||||
document.querySelectorAll(".gallery-grid a").forEach((link, index) => {
|
||||
link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
show(index);
|
||||
});
|
||||
});
|
||||
|
||||
lightbox.querySelector(".site-lightbox-close").addEventListener("click", close);
|
||||
lightbox.querySelector(".site-lightbox-next").addEventListener("click", next);
|
||||
lightbox.querySelector(".site-lightbox-prev").addEventListener("click", prev);
|
||||
|
||||
lightbox.addEventListener("click", (event) => {
|
||||
if (event.target === lightbox) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
lightbox.addEventListener("touchstart", (event) => {
|
||||
touchStartX = event.changedTouches[0].clientX;
|
||||
}, { passive: true });
|
||||
|
||||
lightbox.addEventListener("touchend", (event) => {
|
||||
const delta = event.changedTouches[0].clientX - touchStartX;
|
||||
|
||||
if (Math.abs(delta) < 40) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (delta < 0) {
|
||||
next();
|
||||
} else {
|
||||
prev();
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (lightbox.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
close();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
next();
|
||||
} else if (event.key === "ArrowLeft") {
|
||||
prev();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initSubmenus() {
|
||||
const mobileQuery = window.matchMedia("(max-width: 800px)");
|
||||
|
||||
document.querySelectorAll(".dropdown").forEach((dropdown) => {
|
||||
const button = dropdown.querySelector(".dropdown-label");
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startsOpen = (
|
||||
document.body.classList.contains("mobile-submenus-open")
|
||||
&& mobileQuery.matches
|
||||
);
|
||||
|
||||
dropdown.classList.toggle("is-open", startsOpen);
|
||||
button.setAttribute("aria-expanded", startsOpen ? "true" : "false");
|
||||
button.addEventListener("click", (event) => {
|
||||
if (!mobileQuery.matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const isOpen = dropdown.classList.toggle("is-open");
|
||||
button.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
||||
});
|
||||
|
||||
dropdown.addEventListener("mouseenter", () => {
|
||||
if (mobileQuery.matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.classList.add("is-open");
|
||||
button.setAttribute("aria-expanded", "true");
|
||||
});
|
||||
|
||||
dropdown.addEventListener("mouseleave", () => {
|
||||
if (mobileQuery.matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.classList.remove("is-open");
|
||||
button.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
|
||||
dropdown.addEventListener("focusin", () => {
|
||||
if (mobileQuery.matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.classList.add("is-open");
|
||||
button.setAttribute("aria-expanded", "true");
|
||||
});
|
||||
|
||||
dropdown.addEventListener("focusout", (event) => {
|
||||
if (
|
||||
mobileQuery.matches
|
||||
|| dropdown.contains(event.relatedTarget)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.classList.remove("is-open");
|
||||
button.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(".external-load-button").forEach((button) => {
|
||||
const key = consentKey(button);
|
||||
|
||||
if (button.dataset.autoLoad === "true" || (key && storageGet(key) === "1")) {
|
||||
loadEmbed(button, false);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const externalButton = event.target.closest(".external-load-button");
|
||||
|
||||
if (externalButton) {
|
||||
loadEmbed(externalButton);
|
||||
}
|
||||
|
||||
const galleryButton = event.target.closest(".gallery-load-button");
|
||||
|
||||
if (galleryButton) {
|
||||
const section = galleryButton.closest(".gallery-section");
|
||||
const grid = section?.querySelector(".gallery-grid");
|
||||
|
||||
if (grid) {
|
||||
grid.hidden = false;
|
||||
}
|
||||
|
||||
galleryButton.closest(".gallery-placeholder")?.remove();
|
||||
}
|
||||
});
|
||||
|
||||
initSubmenus();
|
||||
initGallery();
|
||||
1199
generator/static/style.css
Normal file
1199
generator/static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
3
generator/templates/banner.html
Normal file
3
generator/templates/banner.html
Normal file
@ -0,0 +1,3 @@
|
||||
<section class="banner">
|
||||
{{ content | safe }}
|
||||
</section>
|
||||
75
generator/templates/base.html
Normal file
75
generator/templates/base.html
Normal file
@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html lang="{{ settings.language | default("de") }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ settings.title }}</title>
|
||||
<style>
|
||||
:root {
|
||||
--color-background: {{ settings.colors.background | e }};
|
||||
--color-text: {{ settings.colors.text | e }};
|
||||
--color-link: {{ settings.colors.link | e }};
|
||||
--color-link-hover: {{ settings.colors.link_hover | e }};
|
||||
--color-surface: {{ settings.colors.surface | e }};
|
||||
--color-surface-alt: {{ settings.colors.surface_alt | e }};
|
||||
--color-muted-text: {{ settings.colors.muted_text | e }};
|
||||
--color-border: {{ settings.colors.border | e }};
|
||||
--color-header-background: {{ settings.colors.header_background | e }};
|
||||
--color-header-hover: {{ settings.colors.header_hover | e }};
|
||||
--color-header-text: {{ settings.colors.header_text | e }};
|
||||
--color-footer-background: {{ settings.colors.footer_background | e }};
|
||||
--color-footer-text: {{ settings.colors.footer_text | e }};
|
||||
--color-hero-background: {{ settings.colors.hero_background | e }};
|
||||
--color-accent: {{ settings.colors.accent | e }};
|
||||
--font-body: {{ settings.typography.body | default("system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif") | e }};
|
||||
--font-heading: {{ settings.typography.heading | default(settings.typography.body | default("system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif")) | e }};
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="{{ static_path }}">
|
||||
</head>
|
||||
<body class="nav-height-{{ settings.navigation.height | default("medium") }} nav-position-{{ settings.navigation.position | default("sticky") }} nav-align-{{ settings.navigation.align | default("center") }} mobile-menu-{{ settings.mobile_menu.align | default("auto") }} mobile-submenus-{{ settings.mobile_menu.submenus | default("closed") }} font-{{ settings.typography.font | default("system") }} font-size-{{ settings.typography.size | default("medium") }} icons-{{ settings.icons.style | default("round") }} images-{{ "rounded" if settings.gallery.rounded | default(true) else "square" }}{% if logo.enabled %} logo-placement-{{ "header" if logo.header else "footer" }} logo-align-{{ logo.align | default("left") }}{% endif %}">
|
||||
<header class="site-header">
|
||||
<div class="nav-wrap">
|
||||
{% if logo.enabled and logo.header %}
|
||||
<a class="site-logo site-logo-nav" href="{{ navigation[0].url if navigation }}">
|
||||
{% if logo.url %}
|
||||
<img src="{{ logo.url }}" alt="{{ logo.alt }}">
|
||||
{% else %}
|
||||
<span>{{ logo.text }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<input type="checkbox" id="nav-toggle" class="nav-toggle">
|
||||
<label for="nav-toggle" class="burger">☰</label>
|
||||
|
||||
<nav class="main-nav">
|
||||
{% for item in navigation %}
|
||||
{% if item.type == "link" %}
|
||||
<div class="nav-item">
|
||||
<a href="{{ item.url }}">{{ item.title }}</a>
|
||||
</div>
|
||||
{% elif item.type == "dropdown" %}
|
||||
<div class="nav-item dropdown">
|
||||
<button class="dropdown-label" type="button" aria-expanded="{{ "true" if settings.mobile_menu.submenus | default("open") == "open" else "false" }}">{{ item.title }}</button>
|
||||
|
||||
<div class="submenu">
|
||||
{% for child in item.children %}
|
||||
<a href="{{ child.url }}">{{ child.title }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="site-main">
|
||||
{{ body | safe }}
|
||||
</main>
|
||||
|
||||
{{ footer | safe }}
|
||||
|
||||
<script type="module" src="{{ static_path | replace('style.css', 'external-content.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
11
generator/templates/cards.html
Normal file
11
generator/templates/cards.html
Normal file
@ -0,0 +1,11 @@
|
||||
<section class="cards-section cards-layout-{{ layout }}">
|
||||
{{ title | safe }}
|
||||
|
||||
<div class="cards">
|
||||
{% for card in cards %}
|
||||
<article class="card">
|
||||
{{ card | safe }}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
5
generator/templates/contact.html
Normal file
5
generator/templates/contact.html
Normal file
@ -0,0 +1,5 @@
|
||||
<section class="contact-section">
|
||||
<div class="contact-box">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</section>
|
||||
13
generator/templates/downloads.html
Normal file
13
generator/templates/downloads.html
Normal file
@ -0,0 +1,13 @@
|
||||
<section class="downloads-section">
|
||||
<div class="downloads-title">
|
||||
{{ title | safe }}
|
||||
</div>
|
||||
|
||||
<div class="download-list">
|
||||
{% for link in links %}
|
||||
<a class="download-item" href="{{ link.href }}">
|
||||
<span>{{ link.label }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
22
generator/templates/event.html
Normal file
22
generator/templates/event.html
Normal file
@ -0,0 +1,22 @@
|
||||
<section class="event-section">
|
||||
<article class="event-box{% if image %} event-has-image{% endif %}">
|
||||
<div class="event-content">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
|
||||
<dl class="event-facts">
|
||||
{% for key, value in values.items() %}
|
||||
<div>
|
||||
<dt>{{ key.replace("_", " ").title() }}</dt>
|
||||
<dd>{{ value | safe }}</dd>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
|
||||
{% if image %}
|
||||
<figure class="event-image">
|
||||
<img src="{{ image.src }}" alt="{{ image.alt }}">
|
||||
</figure>
|
||||
{% endif %}
|
||||
</article>
|
||||
</section>
|
||||
28
generator/templates/events.html
Normal file
28
generator/templates/events.html
Normal file
@ -0,0 +1,28 @@
|
||||
<section class="event-section">
|
||||
{{ title | safe }}
|
||||
|
||||
<div class="event-list">
|
||||
{% for event in events %}
|
||||
<article class="event-box{% if event.image %} event-has-image{% endif %}">
|
||||
<div class="event-content">
|
||||
{{ event.content | safe }}
|
||||
</div>
|
||||
|
||||
<dl class="event-facts">
|
||||
{% for key, value in event.facts.items() %}
|
||||
<div>
|
||||
<dt>{{ key.replace("_", " ").title() }}</dt>
|
||||
<dd>{{ value | safe }}</dd>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
|
||||
{% if event.image %}
|
||||
<figure class="event-image">
|
||||
<img src="{{ event.image.src }}" alt="{{ event.image.alt }}">
|
||||
</figure>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
11
generator/templates/faq.html
Normal file
11
generator/templates/faq.html
Normal file
@ -0,0 +1,11 @@
|
||||
<section class="faq">
|
||||
<div class="faq-title">
|
||||
{{ title | safe }}
|
||||
</div>
|
||||
|
||||
{% for item in items %}
|
||||
<article class="faq-item">
|
||||
{{ item | safe }}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
44
generator/templates/footer.html
Normal file
44
generator/templates/footer.html
Normal file
@ -0,0 +1,44 @@
|
||||
<footer class="site-footer">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-brand">
|
||||
{{ title | safe }}
|
||||
</div>
|
||||
|
||||
<div class="footer-grid">
|
||||
{% if logo and logo.enabled and logo.footer %}
|
||||
<div class="footer-column footer-logo-column">
|
||||
<a class="site-logo footer-logo" href="{{ logo.home_url if logo.home_url }}">
|
||||
{% if logo.url %}
|
||||
<img src="{{ logo.url }}" alt="{{ logo.alt }}">
|
||||
{% else %}
|
||||
<span>{{ logo.text }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for column in columns %}
|
||||
<div class="footer-column">
|
||||
{{ column | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if social_links %}
|
||||
<div class="footer-column footer-social-column">
|
||||
<h2>Social</h2>
|
||||
<div class="social-links">
|
||||
{% for social in social_links %}
|
||||
<a href="{{ social.url }}" aria-label="{{ social.label }}">
|
||||
<span>{{ social.icon }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p class="footer-credit">
|
||||
Erstellt mit Havelseiten von Binnenrevier
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
42
generator/templates/gallery.html
Normal file
42
generator/templates/gallery.html
Normal file
@ -0,0 +1,42 @@
|
||||
<section class="gallery-section gallery-layout-{{ gallery.layout | default("square") }}">
|
||||
<div class="gallery-title">
|
||||
{{ title | safe }}
|
||||
</div>
|
||||
|
||||
{% if needs_external_consent %}
|
||||
<div class="gallery-placeholder external-placeholder">
|
||||
<button
|
||||
type="button"
|
||||
class="gallery-load-button"
|
||||
>
|
||||
Externe Galerie-Bilder laden
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="gallery-grid"{% if needs_external_consent %} hidden{% endif %}>
|
||||
{% for image in images %}
|
||||
<a
|
||||
href="{{ image.src }}"
|
||||
class="gallery-link"
|
||||
data-pswp-width="{{ image.width }}"
|
||||
data-pswp-height="{{ image.height }}"
|
||||
aria-label="Bild öffnen"
|
||||
>
|
||||
<figure class="gallery-item">
|
||||
<img
|
||||
src="{{ image.src }}"
|
||||
alt="{{ image.alt }}"
|
||||
loading="lazy"
|
||||
>
|
||||
|
||||
{% if image.alt and gallery.captions | default(true) %}
|
||||
<figcaption>
|
||||
{{ image.alt }}
|
||||
</figcaption>
|
||||
{% endif %}
|
||||
</figure>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
44
generator/templates/hero.html
Normal file
44
generator/templates/hero.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% set hero_classes = "hero" %}
|
||||
{% if hero %}
|
||||
{% set hero_classes = hero_classes ~ " hero-has-media hero-" ~ hero.type %}
|
||||
{% endif %}
|
||||
|
||||
<section
|
||||
class="{{ hero_classes }}"
|
||||
{% if hero and hero.style %}style="{{ hero.style | e }}"{% endif %}
|
||||
>
|
||||
{% if hero and hero.type == "image" %}
|
||||
<img class="hero-media" src="{{ hero.src | e }}" alt="">
|
||||
{% elif hero and hero.type == "video" %}
|
||||
<video class="hero-media" src="{{ hero.src | e }}" autoplay muted loop playsinline></video>
|
||||
{% elif hero and hero.type == "youtube" and external_content_enabled %}
|
||||
<iframe
|
||||
class="hero-media hero-youtube-frame"
|
||||
src="{{ hero.src | e }}"
|
||||
title="YouTube-Hero"
|
||||
allow="autoplay; encrypted-media; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
{% elif hero and hero.type == "youtube" %}
|
||||
<div class="hero-media external-placeholder">
|
||||
<div>
|
||||
<strong>Externes YouTube-Video</strong>
|
||||
<p>Dieses Video wird aus Datenschutzgruenden nicht automatisch geladen.</p>
|
||||
<button
|
||||
class="external-load-button"
|
||||
type="button"
|
||||
data-embed-src="{{ hero.src | e }}"
|
||||
data-embed-class="hero-media hero-youtube-frame"
|
||||
data-embed-title="YouTube-Hero"
|
||||
data-embed-allow="autoplay; encrypted-media; picture-in-picture"
|
||||
>
|
||||
Video laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="hero-content">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</section>
|
||||
11
generator/templates/image-text.html
Normal file
11
generator/templates/image-text.html
Normal file
@ -0,0 +1,11 @@
|
||||
<section class="image-text">
|
||||
{% if image %}
|
||||
<div class="image-text-image">
|
||||
<img src="{{ image.src }}" alt="{{ image.alt }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="image-text-content">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</section>
|
||||
26
generator/templates/location.html
Normal file
26
generator/templates/location.html
Normal file
@ -0,0 +1,26 @@
|
||||
<section class="location-section">
|
||||
<div class="location-content">
|
||||
{{ content | safe }}
|
||||
|
||||
{% if address %}
|
||||
<p class="location-address">{{ address }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if address %}
|
||||
<div class="location-placeholder">
|
||||
<strong>Externe Karte</strong>
|
||||
<p>Die Karte wird aus Datenschutzgruenden nicht automatisch geladen.</p>
|
||||
<button
|
||||
class="external-load-button"
|
||||
type="button"
|
||||
data-map-address="{{ address | e }}"
|
||||
data-embed-class="location-map"
|
||||
data-embed-title="Karte"
|
||||
{% if external_content_enabled %}data-auto-load="true"{% endif %}
|
||||
>
|
||||
Karte laden
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
13
generator/templates/people.html
Normal file
13
generator/templates/people.html
Normal file
@ -0,0 +1,13 @@
|
||||
<section class="people-section">
|
||||
<div class="people-title">
|
||||
{{ title | safe }}
|
||||
</div>
|
||||
|
||||
<div class="people-grid">
|
||||
{% for person in people %}
|
||||
<article class="person-card">
|
||||
{{ person | safe }}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
5
generator/templates/person.html
Normal file
5
generator/templates/person.html
Normal file
@ -0,0 +1,5 @@
|
||||
<section class="person-section">
|
||||
<article class="person-card person-card-single">
|
||||
{{ content | safe }}
|
||||
</article>
|
||||
</section>
|
||||
5
generator/templates/quote.html
Normal file
5
generator/templates/quote.html
Normal file
@ -0,0 +1,5 @@
|
||||
<section class="quote-section">
|
||||
<div class="quote-box">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</section>
|
||||
17
generator/templates/sponsors.html
Normal file
17
generator/templates/sponsors.html
Normal file
@ -0,0 +1,17 @@
|
||||
<section class="sponsors-section">
|
||||
<div class="sponsors-title">
|
||||
{{ title | safe }}
|
||||
</div>
|
||||
|
||||
<div class="sponsor-grid">
|
||||
{% for sponsor in sponsors %}
|
||||
<figure class="sponsor-logo">
|
||||
<img src="{{ sponsor.src }}" alt="{{ sponsor.alt }}">
|
||||
|
||||
{% if sponsor.alt %}
|
||||
<figcaption>{{ sponsor.alt }}</figcaption>
|
||||
{% endif %}
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
3
generator/templates/text.html
Normal file
3
generator/templates/text.html
Normal file
@ -0,0 +1,3 @@
|
||||
<section class="text-section">
|
||||
{{ content | safe }}
|
||||
</section>
|
||||
11
generator/templates/timeline.html
Normal file
11
generator/templates/timeline.html
Normal file
@ -0,0 +1,11 @@
|
||||
<section class="timeline">
|
||||
<div class="timeline-title">
|
||||
{{ title | safe }}
|
||||
</div>
|
||||
|
||||
{% for item in items %}
|
||||
<article class="timeline-item">
|
||||
{{ item | safe }}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
13
generator/templates/two-columns.html
Normal file
13
generator/templates/two-columns.html
Normal file
@ -0,0 +1,13 @@
|
||||
<section class="two-columns">
|
||||
<div class="two-columns-title">
|
||||
{{ title | safe }}
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
{{ left | safe }}
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
{{ right | safe }}
|
||||
</div>
|
||||
</section>
|
||||
17
generator/themes/dunkel.json
Normal file
17
generator/themes/dunkel.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"background": "#0f172a",
|
||||
"text": "#e5e7eb",
|
||||
"link": "#38bdf8",
|
||||
"link_hover": "#7dd3fc",
|
||||
"surface": "#111827",
|
||||
"surface_alt": "#1f2937",
|
||||
"muted_text": "#cbd5e1",
|
||||
"border": "#334155",
|
||||
"header_background": "#020617",
|
||||
"header_hover": "#111827",
|
||||
"header_text": "#ffffff",
|
||||
"footer_background": "#020617",
|
||||
"footer_text": "#ffffff",
|
||||
"hero_background": "#1f2937",
|
||||
"accent": "#38bdf8"
|
||||
}
|
||||
17
generator/themes/havel.json
Normal file
17
generator/themes/havel.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"background": "#ffffff",
|
||||
"text": "#111827",
|
||||
"link": "#0f766e",
|
||||
"link_hover": "#115e59",
|
||||
"surface": "#ffffff",
|
||||
"surface_alt": "#f1f5f9",
|
||||
"muted_text": "#4b5563",
|
||||
"border": "#dddddd",
|
||||
"header_background": "#111827",
|
||||
"header_hover": "#1f2937",
|
||||
"header_text": "#ffffff",
|
||||
"footer_background": "#111827",
|
||||
"footer_text": "#ffffff",
|
||||
"hero_background": "#f1f5f9",
|
||||
"accent": "#111827"
|
||||
}
|
||||
17
generator/themes/kueste.json
Normal file
17
generator/themes/kueste.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"background": "#fff8ed",
|
||||
"text": "#243447",
|
||||
"link": "#0f8a9d",
|
||||
"link_hover": "#0b6674",
|
||||
"surface": "#ffffff",
|
||||
"surface_alt": "#d8f3ef",
|
||||
"muted_text": "#68717c",
|
||||
"border": "#f3d6ad",
|
||||
"header_background": "#0b5563",
|
||||
"header_hover": "#0f6f7d",
|
||||
"header_text": "#ffffff",
|
||||
"footer_background": "#0b5563",
|
||||
"footer_text": "#ffffff",
|
||||
"hero_background": "#fde6bf",
|
||||
"accent": "#f59e0b"
|
||||
}
|
||||
17
generator/themes/leuchtturm.json
Normal file
17
generator/themes/leuchtturm.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"background": "#fff7f1",
|
||||
"text": "#2b1d1d",
|
||||
"link": "#b91c1c",
|
||||
"link_hover": "#7f1d1d",
|
||||
"surface": "#ffffff",
|
||||
"surface_alt": "#ffe3d3",
|
||||
"muted_text": "#665b57",
|
||||
"border": "#fca5a5",
|
||||
"header_background": "#1f2937",
|
||||
"header_hover": "#b91c1c",
|
||||
"header_text": "#ffffff",
|
||||
"footer_background": "#1f2937",
|
||||
"footer_text": "#ffffff",
|
||||
"hero_background": "#fecaca",
|
||||
"accent": "#dc2626"
|
||||
}
|
||||
17
generator/themes/segel.json
Normal file
17
generator/themes/segel.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"background": "#f7fbff",
|
||||
"text": "#111c2e",
|
||||
"link": "#2563eb",
|
||||
"link_hover": "#1d4ed8",
|
||||
"surface": "#ffffff",
|
||||
"surface_alt": "#e8eefc",
|
||||
"muted_text": "#536071",
|
||||
"border": "#bfdbfe",
|
||||
"header_background": "#111827",
|
||||
"header_hover": "#1e3a8a",
|
||||
"header_text": "#ffffff",
|
||||
"footer_background": "#111827",
|
||||
"footer_text": "#ffffff",
|
||||
"hero_background": "#dbeafe",
|
||||
"accent": "#f97316"
|
||||
}
|
||||
17
generator/themes/sonnenuntergang.json
Normal file
17
generator/themes/sonnenuntergang.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"background": "#fffaf5",
|
||||
"text": "#1f2937",
|
||||
"link": "#c2410c",
|
||||
"link_hover": "#9a3412",
|
||||
"surface": "#ffffff",
|
||||
"surface_alt": "#ffedd5",
|
||||
"muted_text": "#57534e",
|
||||
"border": "#fed7aa",
|
||||
"header_background": "#7c2d12",
|
||||
"header_hover": "#9a3412",
|
||||
"header_text": "#ffffff",
|
||||
"footer_background": "#7c2d12",
|
||||
"footer_text": "#ffffff",
|
||||
"hero_background": "#ffedd5",
|
||||
"accent": "#c2410c"
|
||||
}
|
||||
17
generator/themes/wald.json
Normal file
17
generator/themes/wald.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"background": "#fbfdf8",
|
||||
"text": "#1f2937",
|
||||
"link": "#15803d",
|
||||
"link_hover": "#166534",
|
||||
"surface": "#ffffff",
|
||||
"surface_alt": "#dcfce7",
|
||||
"muted_text": "#4b5563",
|
||||
"border": "#bbf7d0",
|
||||
"header_background": "#14532d",
|
||||
"header_hover": "#166534",
|
||||
"header_text": "#ffffff",
|
||||
"footer_background": "#14532d",
|
||||
"footer_text": "#ffffff",
|
||||
"hero_background": "#dcfce7",
|
||||
"accent": "#15803d"
|
||||
}
|
||||
17
generator/themes/wasser.json
Normal file
17
generator/themes/wasser.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"background": "#f8fafc",
|
||||
"text": "#0f172a",
|
||||
"link": "#0369a1",
|
||||
"link_hover": "#075985",
|
||||
"surface": "#ffffff",
|
||||
"surface_alt": "#e0f2fe",
|
||||
"muted_text": "#475569",
|
||||
"border": "#bae6fd",
|
||||
"header_background": "#0c4a6e",
|
||||
"header_hover": "#075985",
|
||||
"header_text": "#ffffff",
|
||||
"footer_background": "#0c4a6e",
|
||||
"footer_text": "#ffffff",
|
||||
"hero_background": "#e0f2fe",
|
||||
"accent": "#0369a1"
|
||||
}
|
||||
836
generator/validate.py
Normal file
836
generator/validate.py
Normal file
@ -0,0 +1,836 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import difflib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import settings_config
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
CONTENT_DIR = settings_config.active_content_dir()
|
||||
GENERATOR_DIR = ROOT_DIR / "generator"
|
||||
COMPONENTS_FILE = GENERATOR_DIR / "components.json"
|
||||
SETTINGS_FILE = settings_config.SETTINGS_JSON_FILE
|
||||
SETTINGS_MARKDOWN_FILE = settings_config.active_settings_file() or settings_config.SETTINGS_MARKDOWN_FILE
|
||||
|
||||
MEDIA_DIR_NAME = "medien"
|
||||
LEGACY_MEDIA_DIR_NAME = "assets"
|
||||
GALLERY_DIR_NAME = "galerie"
|
||||
LEGACY_GALLERY_DIR_NAME = "gallery"
|
||||
FOOTER_DIR_NAME = "fusszeile"
|
||||
LEGACY_FOOTER_DIR_NAME = "footer"
|
||||
FOOTER_FILE_NAME = "fusszeile.md"
|
||||
LEGACY_FOOTER_FILE_NAME = "footer.md"
|
||||
|
||||
IMAGE_EXTENSIONS = {
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".webp",
|
||||
".svg"
|
||||
}
|
||||
|
||||
VIDEO_EXTENSIONS = {
|
||||
".mp4",
|
||||
".webm",
|
||||
".ogg",
|
||||
".mov"
|
||||
}
|
||||
|
||||
|
||||
TRANSLATIONS = {
|
||||
"de": {
|
||||
"json_error": "FEHLER: {path} ist keine gueltige JSON-Datei.",
|
||||
"json_position": " Zeile {line}, Spalte {column}: {message}",
|
||||
"navigation_height": "FEHLER: generator/settings.json: navigation.height muss small, medium oder large sein.",
|
||||
"navigation_align": "FEHLER: Einstellungen: Navigationsausrichtung muss links, mittig oder rechts sein.",
|
||||
"mobile_menu_align": "FEHLER: Einstellungen: Mobilmenü muss automatisch, links, mittig oder rechts sein.",
|
||||
"mobile_menu_submenus": "FEHLER: Einstellungen: Mobilmenü-Untermenüs müssen offen oder eingeklappt sein.",
|
||||
"language": "FEHLER: generator/settings.json: language muss de oder en sein.",
|
||||
"validator_language": "FEHLER: generator/settings.json: validator_language muss de oder en sein.",
|
||||
"theme": "FEHLER: Einstellungen: theme muss eine bekannte Farbpalette sein: {themes}.",
|
||||
"theme_multiple": "WARNUNG: havelseite/einstellungen.md: Mehrere Farbpaletten sind angekreuzt ({selected}). Ich verwende die erste: {first}.",
|
||||
"navigation_position": "FEHLER: Einstellungen: Navigation muss sticky/bleibt oben oder static/scrollt mit sein.",
|
||||
"navigation_multiple": "WARNUNG: havelseite/einstellungen.md: Navigation hat mehrere Optionen angekreuzt ({selected}). Ich verwende die erste: {first}.",
|
||||
"logo_align": "FEHLER: Einstellungen: Wenn das Logo angezeigt wird, muss links oder rechts angekreuzt sein.",
|
||||
"logo_align_center": "FEHLER: Einstellungen: Das Logo kann nur links oder rechts stehen.",
|
||||
"logo_missing": "FEHLER: Logo nicht gefunden: havelseite/medien/{folder}/{src}",
|
||||
"logo_folder_missing": "FEHLER: Logoordner nicht gefunden oder leer: havelseite/medien/{folder}",
|
||||
"logo_multiple": "WARNUNG: havelseite/einstellungen.md: Im Logoordner liegen mehrere Bilder ({files}). Ich verwende alphabetisch das erste: {first}.",
|
||||
"gallery_layout": "FEHLER: Einstellungen: Galerie muss quadratisch, breit oder kacheln sein.",
|
||||
"icon_style": "FEHLER: Einstellungen: Icons muss rund, schlicht oder text sein.",
|
||||
"font": "FEHLER: Einstellungen: Schriftgröße muss klein, mittel oder groß sein.",
|
||||
"unknown_block": "FEHLER: {path}:{line}: @{annotation} kenne ich nicht.{hint}",
|
||||
"did_you_mean": " Meintest du @{suggestion}?",
|
||||
"gallery_missing": "FEHLER: {path}:{line}: Galerieordner fehlt: havelseite/medien/galerie/{parameter}.{hint}",
|
||||
"image_missing": "FEHLER: {path}:{line}: Bild nicht gefunden: {src}.{hint}",
|
||||
"internal_link_missing": "WARNUNG: {path}:{line}: Interner Link zeigt auf keine Seite: [[{label}]].{hint}",
|
||||
"hero_missing": "FEHLER: {path}:{line}: Aufmacher-Datei nicht gefunden: {parameter}",
|
||||
"event_single_multiple": "WARNUNG: {path}:{line}: @{annotation} sieht nach mehreren Veranstaltungen aus. Nutze dafuer @veranstaltungen.",
|
||||
"events_list_single": "WARNUNG: {path}:{line}: @{annotation} ist fuer mehrere Veranstaltungen gedacht. Fuer eine einzelne Veranstaltung nutze @veranstaltung.",
|
||||
"person_single_multiple": "WARNUNG: {path}:{line}: @{annotation} sieht nach mehreren Personen aus. Nutze dafuer @personen.",
|
||||
"people_list_single": "WARNUNG: {path}:{line}: @{annotation} ist fuer mehrere Personen gedacht. Fuer eine einzelne Person nutze @person.",
|
||||
"nested_page": "FEHLER: {path}: Seiten duerfen nur eine Ordnerebene tief liegen. Bitte verschiebe die Datei direkt nach havelseite/ oder in einen Ordner direkt unter havelseite/.",
|
||||
"found_header": "Ich habe ein paar Dinge gefunden:\n",
|
||||
"found_footer": "\nBitte korrigiere diese Punkte und pruefe danach nochmal.",
|
||||
"ok": "Alles gut. Keine offensichtlichen Probleme gefunden."
|
||||
},
|
||||
"en": {
|
||||
"json_error": "ERROR: {path} is not a valid JSON file.",
|
||||
"json_position": " Line {line}, column {column}: {message}",
|
||||
"navigation_height": "ERROR: generator/settings.json: navigation.height must be small, medium, or large.",
|
||||
"navigation_align": "ERROR: Settings: navigation alignment must be left, center, or right.",
|
||||
"mobile_menu_align": "ERROR: Settings: mobile menu must be auto, left, center, or right.",
|
||||
"mobile_menu_submenus": "ERROR: Settings: mobile submenu behavior must be open or closed.",
|
||||
"language": "ERROR: generator/settings.json: language must be de or en.",
|
||||
"validator_language": "ERROR: generator/settings.json: validator_language must be de or en.",
|
||||
"theme": "ERROR: Settings: theme must be a known color theme: {themes}.",
|
||||
"theme_multiple": "WARNING: havelseite/einstellungen.md: Multiple color themes are checked ({selected}). I will use the first one: {first}.",
|
||||
"navigation_position": "ERROR: Settings: navigation.position must be sticky or static.",
|
||||
"navigation_multiple": "WARNING: havelseite/einstellungen.md: Navigation has multiple checked options ({selected}). I will use the first one: {first}.",
|
||||
"logo_align": "ERROR: Settings: if the logo is shown, left or right must be checked.",
|
||||
"logo_align_center": "ERROR: Settings: the logo can only be left or right.",
|
||||
"logo_missing": "ERROR: Logo not found: havelseite/medien/{folder}/{src}",
|
||||
"logo_folder_missing": "ERROR: Logo folder is missing or empty: havelseite/medien/{folder}",
|
||||
"logo_multiple": "WARNING: havelseite/einstellungen.md: Multiple images are in the logo folder ({files}). I will use the alphabetically first one: {first}.",
|
||||
"gallery_layout": "ERROR: Settings: gallery layout must be square, wide, or masonry.",
|
||||
"icon_style": "ERROR: Settings: icons must be round, simple, or text.",
|
||||
"font": "ERROR: Settings: font size must be small, medium, or large.",
|
||||
"unknown_block": "ERROR: {path}:{line}: I do not know @{annotation}.{hint}",
|
||||
"did_you_mean": " Did you mean @{suggestion}?",
|
||||
"gallery_missing": "ERROR: {path}:{line}: Gallery folder is missing: havelseite/medien/galerie/{parameter}.{hint}",
|
||||
"image_missing": "ERROR: {path}:{line}: Image not found: {src}.{hint}",
|
||||
"internal_link_missing": "WARNING: {path}:{line}: Internal link does not point to a known page: [[{label}]].{hint}",
|
||||
"hero_missing": "ERROR: {path}:{line}: Hero file not found: {parameter}",
|
||||
"event_single_multiple": "WARNING: {path}:{line}: @{annotation} looks like multiple events. Use @events for that.",
|
||||
"events_list_single": "WARNING: {path}:{line}: @{annotation} is meant for multiple events. Use @event for a single event.",
|
||||
"person_single_multiple": "WARNING: {path}:{line}: @{annotation} looks like multiple people. Use @people for that.",
|
||||
"people_list_single": "WARNING: {path}:{line}: @{annotation} is meant for multiple people. Use @person for one person.",
|
||||
"nested_page": "ERROR: {path}: Pages may only be one folder deep. Please move this file directly to havelseite/ or into a folder directly below havelseite/.",
|
||||
"found_header": "I found a few things:\n",
|
||||
"found_footer": "\nPlease fix these points and check again.",
|
||||
"ok": "All good. No obvious problems found."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def language_from_settings() -> str:
|
||||
try:
|
||||
settings = settings_config.load_settings()
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return "de"
|
||||
|
||||
language = settings.get(
|
||||
"validator_language",
|
||||
settings.get("language", "de")
|
||||
)
|
||||
|
||||
if language not in TRANSLATIONS:
|
||||
return "de"
|
||||
|
||||
return language
|
||||
|
||||
|
||||
def tr(language: str, key: str, **values) -> str:
|
||||
return TRANSLATIONS[language][key].format(**values)
|
||||
|
||||
|
||||
def load_json(
|
||||
file_path: Path,
|
||||
exit_on_error: bool = True
|
||||
):
|
||||
try:
|
||||
with file_path.open(encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError as error:
|
||||
if not exit_on_error:
|
||||
raise
|
||||
|
||||
language = "de"
|
||||
|
||||
print(tr(
|
||||
language,
|
||||
"json_error",
|
||||
path=file_path.relative_to(ROOT_DIR)
|
||||
))
|
||||
print(tr(
|
||||
language,
|
||||
"json_position",
|
||||
line=error.lineno,
|
||||
column=error.colno,
|
||||
message=error.msg
|
||||
))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def is_external_url(value: str) -> bool:
|
||||
return (
|
||||
value.startswith("http://")
|
||||
or value.startswith("https://")
|
||||
)
|
||||
|
||||
|
||||
def is_footer_file(md_file: Path) -> bool:
|
||||
relative = md_file.relative_to(CONTENT_DIR)
|
||||
|
||||
return (
|
||||
len(relative.parts) >= 2
|
||||
and relative.parts[0] in {FOOTER_DIR_NAME, LEGACY_FOOTER_DIR_NAME}
|
||||
and md_file.name in {FOOTER_FILE_NAME, LEGACY_FOOTER_FILE_NAME}
|
||||
)
|
||||
|
||||
|
||||
def title_from_file(md_file: Path) -> str:
|
||||
stem = display_name(md_file.stem)
|
||||
|
||||
if stem == "index":
|
||||
if md_file.parent == CONTENT_DIR:
|
||||
return "Start"
|
||||
|
||||
return display_name(md_file.parent.name).replace("-", " ").title()
|
||||
|
||||
return stem.replace("-", " ").title()
|
||||
|
||||
|
||||
def display_name(name: str) -> str:
|
||||
return re.sub(r"^\d+[_-]+", "", name)
|
||||
|
||||
|
||||
def slugify_title(title: str) -> str:
|
||||
slug = title.strip().lower()
|
||||
slug = slug.replace("ä", "ae")
|
||||
slug = slug.replace("ö", "oe")
|
||||
slug = slug.replace("ü", "ue")
|
||||
slug = slug.replace("ß", "ss")
|
||||
slug = re.sub(r"[^a-z0-9]+", "-", slug)
|
||||
|
||||
return slug.strip("-")
|
||||
|
||||
|
||||
def page_keys() -> set[str]:
|
||||
keys = set()
|
||||
|
||||
for md_file in CONTENT_DIR.rglob("*.md"):
|
||||
if is_footer_file(md_file):
|
||||
continue
|
||||
|
||||
if md_file == SETTINGS_MARKDOWN_FILE:
|
||||
continue
|
||||
|
||||
keys.add(md_file.stem.lower())
|
||||
keys.add(display_name(md_file.stem).lower())
|
||||
keys.add(slugify_title(title_from_file(md_file)))
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
def page_suggestions() -> list[str]:
|
||||
suggestions = []
|
||||
|
||||
for md_file in CONTENT_DIR.rglob("*.md"):
|
||||
if is_footer_file(md_file):
|
||||
continue
|
||||
|
||||
if md_file == SETTINGS_MARKDOWN_FILE:
|
||||
continue
|
||||
|
||||
suggestions.append(title_from_file(md_file))
|
||||
suggestions.append(display_name(md_file.stem))
|
||||
|
||||
return sorted(set(suggestions))
|
||||
|
||||
|
||||
def gallery_suggestion(parameter: str) -> str:
|
||||
gallery_dir = CONTENT_DIR / MEDIA_DIR_NAME / GALLERY_DIR_NAME
|
||||
|
||||
if not gallery_dir.exists():
|
||||
gallery_dir = CONTENT_DIR / LEGACY_MEDIA_DIR_NAME / LEGACY_GALLERY_DIR_NAME
|
||||
|
||||
if not gallery_dir.exists():
|
||||
return ""
|
||||
|
||||
candidates = [
|
||||
path.name for path in gallery_dir.iterdir()
|
||||
if path.is_dir()
|
||||
]
|
||||
suggestion = difflib.get_close_matches(parameter, candidates, n=1)
|
||||
|
||||
if not suggestion:
|
||||
return ""
|
||||
|
||||
return f" Meintest du {suggestion[0]}?"
|
||||
|
||||
|
||||
def media_suggestion(src: str) -> str:
|
||||
media_dir = CONTENT_DIR / MEDIA_DIR_NAME
|
||||
|
||||
if not media_dir.exists():
|
||||
media_dir = CONTENT_DIR / LEGACY_MEDIA_DIR_NAME
|
||||
|
||||
if not media_dir.exists():
|
||||
return ""
|
||||
|
||||
file_name = Path(src).name
|
||||
candidates = [
|
||||
path.name for path in media_dir.rglob("*")
|
||||
if path.is_file()
|
||||
]
|
||||
suggestion = difflib.get_close_matches(file_name, candidates, n=1)
|
||||
|
||||
if not suggestion:
|
||||
return ""
|
||||
|
||||
matches = [
|
||||
path for path in media_dir.rglob(suggestion[0])
|
||||
if path.is_file()
|
||||
]
|
||||
|
||||
if not matches:
|
||||
return f" Meintest du {suggestion[0]}?"
|
||||
|
||||
try:
|
||||
suggested_path = matches[0].relative_to(CONTENT_DIR).as_posix()
|
||||
except ValueError:
|
||||
suggested_path = matches[0].name
|
||||
|
||||
return f" Meintest du {suggested_path}?"
|
||||
|
||||
|
||||
def content_relative_from_src(src: str, source_file: Path) -> Path:
|
||||
src = src.strip().strip("/")
|
||||
|
||||
variants = [src]
|
||||
|
||||
if src.startswith(f"{LEGACY_MEDIA_DIR_NAME}/"):
|
||||
variants.append(
|
||||
f"{MEDIA_DIR_NAME}/{src.removeprefix(f'{LEGACY_MEDIA_DIR_NAME}/')}"
|
||||
)
|
||||
|
||||
variants = [
|
||||
variant.replace(
|
||||
f"{MEDIA_DIR_NAME}/{LEGACY_GALLERY_DIR_NAME}/",
|
||||
f"{MEDIA_DIR_NAME}/{GALLERY_DIR_NAME}/"
|
||||
)
|
||||
for variant in variants
|
||||
]
|
||||
|
||||
candidates = []
|
||||
|
||||
for variant in dict.fromkeys(variants):
|
||||
candidates.extend([
|
||||
source_file.parent / variant,
|
||||
CONTENT_DIR / variant
|
||||
])
|
||||
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
|
||||
assets_dir = CONTENT_DIR / MEDIA_DIR_NAME
|
||||
|
||||
if not assets_dir.exists():
|
||||
assets_dir = CONTENT_DIR / LEGACY_MEDIA_DIR_NAME
|
||||
|
||||
if assets_dir.exists():
|
||||
matches = sorted([
|
||||
path for path in assets_dir.rglob(Path(src).name)
|
||||
if path.is_file()
|
||||
])
|
||||
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
return CONTENT_DIR / src
|
||||
|
||||
|
||||
def line_number(text: str, offset: int) -> int:
|
||||
return text.count("\n", 0, offset) + 1
|
||||
|
||||
|
||||
def annotated_blocks(text: str) -> list[dict]:
|
||||
matches = list(re.finditer(r"(?m)^@([^\s:]+)(?::([^\n]+))?", text))
|
||||
blocks = []
|
||||
|
||||
for index, match in enumerate(matches):
|
||||
content_start = match.end()
|
||||
content_end = (
|
||||
matches[index + 1].start()
|
||||
if index + 1 < len(matches)
|
||||
else len(text)
|
||||
)
|
||||
blocks.append({
|
||||
"annotation": match.group(1).strip(),
|
||||
"parameter": (match.group(2) or "").strip(),
|
||||
"content": text[content_start:content_end].strip(),
|
||||
"line": line_number(text, match.start())
|
||||
})
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def validate_settings(
|
||||
messages: list[str],
|
||||
language: str
|
||||
):
|
||||
settings = settings_config.load_settings()
|
||||
selected_settings = {}
|
||||
|
||||
if SETTINGS_MARKDOWN_FILE.exists():
|
||||
selected_settings = settings_config.selected_by_section(
|
||||
SETTINGS_MARKDOWN_FILE.read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
if settings.get("language", "de") not in {"de", "en"}:
|
||||
messages.append(tr(language, "language"))
|
||||
|
||||
if settings.get("validator_language", "de") not in {"de", "en"}:
|
||||
messages.append(tr(language, "validator_language"))
|
||||
|
||||
if settings.get("theme", "havel") not in settings_config.COLOR_THEMES:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"theme",
|
||||
themes=", ".join(settings_config.COLOR_THEMES.keys())
|
||||
))
|
||||
|
||||
selected_themes = selected_settings.get("theme", [])
|
||||
|
||||
if len(selected_themes) > 1:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"theme_multiple",
|
||||
selected=", ".join(selected_themes),
|
||||
first=selected_themes[0]
|
||||
))
|
||||
|
||||
navigation_height = (
|
||||
settings
|
||||
.get("navigation", {})
|
||||
.get("height", "medium")
|
||||
)
|
||||
|
||||
if navigation_height not in {"small", "medium", "large"}:
|
||||
messages.append(tr(language, "navigation_height"))
|
||||
|
||||
navigation_position = (
|
||||
settings
|
||||
.get("navigation", {})
|
||||
.get("position", "sticky")
|
||||
)
|
||||
|
||||
if navigation_position not in {"sticky", "static"}:
|
||||
messages.append(tr(language, "navigation_position"))
|
||||
|
||||
navigation_align = (
|
||||
settings
|
||||
.get("navigation", {})
|
||||
.get("align", "center")
|
||||
)
|
||||
|
||||
if navigation_align not in {"left", "center", "right"}:
|
||||
messages.append(tr(language, "navigation_align"))
|
||||
|
||||
mobile_menu_align = (
|
||||
settings
|
||||
.get("mobile_menu", {})
|
||||
.get("align", "auto")
|
||||
)
|
||||
|
||||
if mobile_menu_align not in {"auto", "left", "center", "right"}:
|
||||
messages.append(tr(language, "mobile_menu_align"))
|
||||
|
||||
mobile_menu_submenus = (
|
||||
settings
|
||||
.get("mobile_menu", {})
|
||||
.get("submenus", "open")
|
||||
)
|
||||
|
||||
if mobile_menu_submenus not in {"open", "closed"}:
|
||||
messages.append(tr(language, "mobile_menu_submenus"))
|
||||
|
||||
selected_navigation = selected_settings.get("navigation", [])
|
||||
selected_navigation_heights = [
|
||||
value for value in selected_navigation
|
||||
if settings_config.normalize_choice(
|
||||
value,
|
||||
settings_config.SIZE_ALIASES
|
||||
) in {"small", "medium", "large"}
|
||||
]
|
||||
selected_navigation_positions = [
|
||||
value for value in selected_navigation
|
||||
if settings_config.normalize_choice(
|
||||
value,
|
||||
settings_config.POSITION_ALIASES
|
||||
) in {"sticky", "static"}
|
||||
]
|
||||
selected_navigation_aligns = [
|
||||
value for value in selected_navigation
|
||||
if settings_config.normalize_choice(
|
||||
value,
|
||||
settings_config.ALIGN_ALIASES
|
||||
) in {"left", "center", "right"}
|
||||
]
|
||||
|
||||
if len(selected_navigation_heights) > 1:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"navigation_multiple",
|
||||
selected=", ".join(selected_navigation_heights),
|
||||
first=selected_navigation_heights[0]
|
||||
))
|
||||
|
||||
if len(selected_navigation_positions) > 1:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"navigation_multiple",
|
||||
selected=", ".join(selected_navigation_positions),
|
||||
first=selected_navigation_positions[0]
|
||||
))
|
||||
|
||||
if len(selected_navigation_aligns) > 1:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"navigation_multiple",
|
||||
selected=", ".join(selected_navigation_aligns),
|
||||
first=selected_navigation_aligns[0]
|
||||
))
|
||||
|
||||
logo = settings.get("logo", {})
|
||||
|
||||
if logo.get("enabled", False):
|
||||
align = logo.get("align", "left")
|
||||
selected_logo = selected_settings.get("logo", [])
|
||||
selected_logo_aligns = [
|
||||
value for value in selected_logo
|
||||
if settings_config.normalize_choice(
|
||||
value,
|
||||
settings_config.ALIGN_ALIASES
|
||||
) in {"left", "right"}
|
||||
]
|
||||
|
||||
if not selected_logo_aligns:
|
||||
messages.append(tr(language, "logo_align"))
|
||||
|
||||
elif align not in {"left", "center", "right"}:
|
||||
messages.append(tr(language, "logo_align"))
|
||||
elif align == "center":
|
||||
messages.append(tr(language, "logo_align_center"))
|
||||
|
||||
src = logo.get("src", "").strip()
|
||||
folder = logo.get("folder", "logo").strip().strip("/") or "logo"
|
||||
|
||||
if src:
|
||||
logo_file = CONTENT_DIR / MEDIA_DIR_NAME / folder / src
|
||||
|
||||
if not logo_file.exists():
|
||||
logo_file = CONTENT_DIR / LEGACY_MEDIA_DIR_NAME / folder / src
|
||||
|
||||
if not logo_file.exists():
|
||||
messages.append(tr(
|
||||
language,
|
||||
"logo_missing",
|
||||
folder=folder,
|
||||
src=src
|
||||
))
|
||||
else:
|
||||
logo_dir = CONTENT_DIR / MEDIA_DIR_NAME / folder
|
||||
|
||||
if not logo_dir.exists():
|
||||
logo_dir = CONTENT_DIR / LEGACY_MEDIA_DIR_NAME / folder
|
||||
|
||||
logo_files = sorted([
|
||||
file.name for file in logo_dir.iterdir()
|
||||
if file.is_file() and file.suffix.lower() in IMAGE_EXTENSIONS
|
||||
]) if logo_dir.exists() else []
|
||||
|
||||
if not logo_files:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"logo_folder_missing",
|
||||
folder=folder
|
||||
))
|
||||
elif len(logo_files) > 1:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"logo_multiple",
|
||||
files=", ".join(logo_files),
|
||||
first=logo_files[0]
|
||||
))
|
||||
|
||||
gallery_layout = (
|
||||
settings
|
||||
.get("gallery", {})
|
||||
.get("layout", "square")
|
||||
)
|
||||
|
||||
if gallery_layout not in {"square", "wide", "masonry"}:
|
||||
messages.append(tr(language, "gallery_layout"))
|
||||
|
||||
icon_style = (
|
||||
settings
|
||||
.get("icons", {})
|
||||
.get("style", "round")
|
||||
)
|
||||
|
||||
if icon_style not in {"round", "simple", "text"}:
|
||||
messages.append(tr(language, "icon_style"))
|
||||
|
||||
font_size = (
|
||||
settings
|
||||
.get("typography", {})
|
||||
.get("size", "medium")
|
||||
)
|
||||
|
||||
if font_size not in {"small", "medium", "large"}:
|
||||
messages.append(tr(language, "font"))
|
||||
|
||||
|
||||
def validate_markdown_files(
|
||||
messages: list[str],
|
||||
language: str
|
||||
):
|
||||
components = load_json(COMPONENTS_FILE)
|
||||
known_annotations = set(components.keys())
|
||||
known_pages = page_keys()
|
||||
known_page_suggestions = page_suggestions()
|
||||
|
||||
for md_file in sorted(CONTENT_DIR.rglob("*.md")):
|
||||
if md_file == SETTINGS_MARKDOWN_FILE:
|
||||
continue
|
||||
|
||||
if len(md_file.relative_to(CONTENT_DIR).parts) > 2:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"nested_page",
|
||||
path=md_file.relative_to(ROOT_DIR)
|
||||
))
|
||||
continue
|
||||
|
||||
text = md_file.read_text(encoding="utf-8")
|
||||
display_path = md_file.relative_to(ROOT_DIR)
|
||||
|
||||
for match in re.finditer(r"(?m)^@([^\s:]+)(?::([^\n]+))?", text):
|
||||
annotation = match.group(1).strip()
|
||||
parameter = (match.group(2) or "").strip()
|
||||
line = line_number(text, match.start())
|
||||
|
||||
if annotation not in known_annotations:
|
||||
suggestion = difflib.get_close_matches(
|
||||
annotation,
|
||||
known_annotations,
|
||||
n=1
|
||||
)
|
||||
|
||||
hint = (
|
||||
tr(
|
||||
language,
|
||||
"did_you_mean",
|
||||
suggestion=suggestion[0]
|
||||
)
|
||||
if suggestion
|
||||
else ""
|
||||
)
|
||||
|
||||
messages.append(tr(
|
||||
language,
|
||||
"unknown_block",
|
||||
path=display_path,
|
||||
line=line,
|
||||
annotation=annotation,
|
||||
hint=hint
|
||||
))
|
||||
|
||||
component = components.get(annotation, annotation)
|
||||
|
||||
if component == "gallery" and parameter:
|
||||
gallery_dir = CONTENT_DIR / MEDIA_DIR_NAME / GALLERY_DIR_NAME / parameter
|
||||
|
||||
if not gallery_dir.exists():
|
||||
gallery_dir = (
|
||||
CONTENT_DIR
|
||||
/ LEGACY_MEDIA_DIR_NAME
|
||||
/ LEGACY_GALLERY_DIR_NAME
|
||||
/ parameter
|
||||
)
|
||||
|
||||
if not gallery_dir.exists():
|
||||
messages.append(tr(
|
||||
language,
|
||||
"gallery_missing",
|
||||
path=display_path,
|
||||
line=line,
|
||||
parameter=parameter,
|
||||
hint=gallery_suggestion(parameter)
|
||||
))
|
||||
|
||||
if component == "hero" and parameter:
|
||||
validate_media_parameter(
|
||||
messages,
|
||||
display_path,
|
||||
line,
|
||||
parameter,
|
||||
md_file,
|
||||
language
|
||||
)
|
||||
|
||||
for block in annotated_blocks(text):
|
||||
component = components.get(block["annotation"], block["annotation"])
|
||||
h2_count = len(re.findall(r"(?m)^##\s+", block["content"]))
|
||||
|
||||
if component == "event" and h2_count > 1:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"event_single_multiple",
|
||||
path=display_path,
|
||||
line=block["line"],
|
||||
annotation=block["annotation"]
|
||||
))
|
||||
|
||||
if component == "events" and h2_count < 2:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"events_list_single",
|
||||
path=display_path,
|
||||
line=block["line"],
|
||||
annotation=block["annotation"]
|
||||
))
|
||||
|
||||
if component == "person" and h2_count > 1:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"person_single_multiple",
|
||||
path=display_path,
|
||||
line=block["line"],
|
||||
annotation=block["annotation"]
|
||||
))
|
||||
|
||||
if component == "people" and h2_count < 2:
|
||||
messages.append(tr(
|
||||
language,
|
||||
"people_list_single",
|
||||
path=display_path,
|
||||
line=block["line"],
|
||||
annotation=block["annotation"]
|
||||
))
|
||||
|
||||
for match in re.finditer(r"!\[(.*?)\]\((.*?)\)", text):
|
||||
src = match.group(2).strip()
|
||||
|
||||
if is_external_url(src):
|
||||
continue
|
||||
|
||||
file_path = content_relative_from_src(src, md_file)
|
||||
|
||||
if not file_path.exists():
|
||||
messages.append(tr(
|
||||
language,
|
||||
"image_missing",
|
||||
path=display_path,
|
||||
line=line_number(text, match.start()),
|
||||
src=src,
|
||||
hint=media_suggestion(src)
|
||||
))
|
||||
|
||||
for match in re.finditer(r"\[\[(.*?)\]\]", text):
|
||||
label = match.group(1).strip()
|
||||
key = slugify_title(label)
|
||||
|
||||
if key not in known_pages:
|
||||
suggestion = difflib.get_close_matches(
|
||||
label,
|
||||
known_page_suggestions,
|
||||
n=1
|
||||
)
|
||||
hint = (
|
||||
tr(
|
||||
language,
|
||||
"did_you_mean",
|
||||
suggestion=suggestion[0]
|
||||
)
|
||||
if suggestion
|
||||
else ""
|
||||
)
|
||||
|
||||
messages.append(tr(
|
||||
language,
|
||||
"internal_link_missing",
|
||||
path=display_path,
|
||||
line=line_number(text, match.start()),
|
||||
label=label,
|
||||
hint=hint
|
||||
))
|
||||
|
||||
|
||||
def validate_media_parameter(
|
||||
messages: list[str],
|
||||
display_path: Path,
|
||||
line: int,
|
||||
parameter: str,
|
||||
md_file: Path,
|
||||
language: str
|
||||
):
|
||||
parsed = urlparse(parameter)
|
||||
suffix = Path(parsed.path).suffix.lower()
|
||||
|
||||
if is_external_url(parameter):
|
||||
return
|
||||
|
||||
if suffix in IMAGE_EXTENSIONS or suffix in VIDEO_EXTENSIONS:
|
||||
file_path = content_relative_from_src(parameter, md_file)
|
||||
|
||||
if not file_path.exists():
|
||||
messages.append(tr(
|
||||
language,
|
||||
"hero_missing",
|
||||
path=display_path,
|
||||
line=line,
|
||||
parameter=parameter
|
||||
))
|
||||
|
||||
|
||||
def collect_messages() -> tuple[list[str], str]:
|
||||
messages: list[str] = []
|
||||
settings_config.format_settings_file()
|
||||
language = language_from_settings()
|
||||
|
||||
validate_settings(messages, language)
|
||||
validate_markdown_files(messages, language)
|
||||
|
||||
return messages, language
|
||||
|
||||
|
||||
def main() -> int:
|
||||
messages, language = collect_messages()
|
||||
|
||||
if messages:
|
||||
print(tr(language, "found_header"))
|
||||
|
||||
for message in messages:
|
||||
print(f"- {message}")
|
||||
|
||||
print(tr(language, "found_footer"))
|
||||
|
||||
return 1 if has_errors(messages) else 0
|
||||
|
||||
print(tr(language, "ok"))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def has_errors(messages: list[str]) -> bool:
|
||||
return any(
|
||||
not (
|
||||
message.startswith("WARNUNG:")
|
||||
or message.startswith("WARNING:")
|
||||
)
|
||||
for message in messages
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user