From e1fca0eb1ef76882d34e6232beb1faa95973c770 Mon Sep 17 00:00:00 2001 From: thek4n Date: Wed, 3 Jun 2026 14:15:55 +0300 Subject: [PATCH] feat: implement main logic --- Cargo.lock | 217 ++++++++++++++ Cargo.toml | 12 + TODO.md | 18 +- assets/script.js | 67 +---- docs/config_example.toml | 46 ++- src/client/detector.rs | 63 ++++ src/client/mod.rs | 4 + src/config/mod.rs | 12 + src/config/models.rs | 70 +++++ src/handlers/mod.rs | 3 + src/handlers/root.rs | 105 +++++++ src/language/detector.rs | 67 +++++ src/language/mod.rs | 3 + src/main.rs | 569 +++++++----------------------------- src/templates/builders.rs | 236 +++++++++++++++ src/templates/mod.rs | 3 + templates/html_index.jinja2 | 45 ++- templates/text_index.jinja2 | 14 +- 18 files changed, 996 insertions(+), 558 deletions(-) create mode 100644 src/client/detector.rs create mode 100644 src/client/mod.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/models.rs create mode 100644 src/handlers/mod.rs create mode 100644 src/handlers/root.rs create mode 100644 src/language/detector.rs create mode 100644 src/language/mod.rs create mode 100644 src/templates/builders.rs create mode 100644 src/templates/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 35837cb..b33ecc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "askama" version = "0.16.0" @@ -143,6 +193,15 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -184,12 +243,96 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -254,6 +397,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "glob" version = "0.3.3" @@ -266,6 +419,18 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.1" @@ -380,6 +545,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" @@ -463,6 +634,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "parking_lot" version = "0.12.5" @@ -624,6 +801,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "2.0.1" @@ -662,6 +850,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.117" @@ -686,7 +880,12 @@ dependencies = [ "askama", "axum", "chrono", + "clap", + "clap_complete", + "hex", + "mime", "serde", + "sha2", "tokio", "toml", ] @@ -808,12 +1007,30 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 0ca7ded..7b57647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,13 @@ edition = "2024" authors = ["Vladislav Kan "] license = "MIT" +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true + [dependencies] askama = "0.16.0" serde = { version = "1.0", features = ["derive"] } @@ -12,3 +19,8 @@ toml = "0.8" axum = "0.8" tokio = { version = "1", features = ["full"] } chrono = "0.4" +clap = { version = "4.5", features = ["derive"] } +clap_complete = "4.5" +sha2 = "0.10" +hex = "0.4" +mime = "0.3" diff --git a/TODO.md b/TODO.md index d63791e..bcd8a1b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,17 @@ # ToDo list -* [ ] Сделать в embed шаблоны -* [ ] Сделать автоматическую локализацию по ip или заголовкам http `Accept-Language` +* [ ] Поправить стили +* [ ] alert copied +* [X] сделать путь до ключа +* [X] Сделать стили и засунуть их в embed (с уникальным хешем в имени) +* [X] Сделать js и засунуть их в embed (с уникальным хешем в имени) +* [ ] Добавить валидирование конфига +* [ ] Сделать default config чтобы выводился по аргументу cli +* [X] Сделать автоматическую локализацию по ip или заголовкам http `Accept-Language` * [X] Сделать локализацию по параметру `/?lang=en` * [X] Сделать авто выбор верстки в зависимости от User-Agent -* [ ] Сделать стили и засунуть их в embed (с уникальным хешем в имени) -* [ ] Сделать скрипты и засунуть их в embed (с уникальным хешем в имени) -* [ ] Отрефачить -* [ ] Сделать определение хоста по которому был запрос и засунуть его в +* [X] Отрефачить +* [X] Сделать определение хоста по которому был запрос и засунуть его в аргумент `build_html_template()` * [x] Сделать текущий год на клиенте или сервере (решить) (на сервере предпочтительно, чтобы им нельзя было манипулировать на клиенте) -* [ ] Вынести конфиг в параметры +* [X] Вынести конфиг в параметры diff --git a/assets/script.js b/assets/script.js index aa931a1..d78db8d 100644 --- a/assets/script.js +++ b/assets/script.js @@ -1,64 +1,17 @@ -const translations = { - en: document.querySelectorAll('[data-lang="en"]'), - ru: document.querySelectorAll('[data-lang="ru"]'), -}; - -function setLanguage(lang) { - document.documentElement.lang = lang; - localStorage.setItem("lang", lang); - - const langToHide = lang === "en" ? "ru" : "en"; - - translations[langToHide].forEach((el) => (el.style.display = "none")); - translations[lang].forEach((el) => (el.style.display = "")); - - document.getElementById(`lang-${lang}`).classList.add("active"); - document.getElementById(`lang-${langToHide}`).classList.remove("active"); - - if (window.event) event.preventDefault(); -} - -function setTheme(theme) { - if (theme === "new") { - document.documentElement.setAttribute("data-theme", "new"); - } else { - document.documentElement.removeAttribute("data-theme"); - } - - localStorage.setItem("theme", theme); - - document - .getElementById("theme-old") - .classList.toggle("active", theme === "old"); - document - .getElementById("theme-new") - .classList.toggle("active", theme === "new"); - - if (window.event) event.preventDefault(); -} - function copyGPGKey() { var e = window.event; if (e) { e.preventDefault(); e.stopPropagation(); } const keyText = document.getElementById('gpg-key').textContent; navigator.clipboard.writeText(keyText).then(() => { - const btnEn = document.getElementById('copy-gpg'); - const btnRu = document.getElementById('copy-gpg-ru'); - const prevEn = btnEn.textContent; - const prevRu = btnRu.textContent; - btnEn.textContent = 'Copied!'; - btnRu.textContent = 'Скопировано!'; - setTimeout(() => { - btnEn.textContent = prevEn; - btnRu.textContent = prevRu; - }, 2000); + // const btnEn = document.getElementById('copy-gpg'); + // const btnRu = document.getElementById('copy-gpg-ru'); + // const prevEn = btnEn.textContent; + // const prevRu = btnRu.textContent; + // btnEn.textContent = 'Copied!'; + // btnRu.textContent = 'Скопировано!'; + // setTimeout(() => { + // btnEn.textContent = prevEn; + // btnRu.textContent = prevRu; + // }, 2000); }); } - -document.addEventListener("DOMContentLoaded", () => { - const savedLang = localStorage.getItem("lang") || "en"; - const savedTheme = localStorage.getItem("theme") || "old"; - - setLanguage(savedLang); - setTheme(savedTheme); -}); diff --git a/docs/config_example.toml b/docs/config_example.toml index 858af1f..52117e7 100644 --- a/docs/config_example.toml +++ b/docs/config_example.toml @@ -1,10 +1,12 @@ [page] title.en = "Hi, I'm Vlad" title.ru = "Владислав Кан" +title.ua = "Владислав Кан" meta_title = "thek4n" meta_description = "thek4n - developer" description.en = "Developer" description.ru = "Разработчик" +description.ua = "Разработчик" copyright_author = "Vladislav Kan " @@ -20,6 +22,10 @@ title = "english" key = "ru" title = "русский" +[[languages]] +key = "ua" +title = "український" + # --- @@ -27,12 +33,29 @@ title = "русский" [page.aboutme] title.en = "About Me" title.ru = "Обо Мне" +title.ua = "Про мене" text.en = ''' -About me text... +I am a Software Engineer 👨‍💻. + +I'm working on developing of various IT products from embedded ⚙️ to web 🌐. + +I enjoy reading different tech articles and books 📘. ''' text.ru = ''' -Обо мне текст... +Я инженер-программист 👨💻. + +Я занимаюсь разработкой различных ИТ-продуктов, от embedded ⚙️ до веб-приложений 🌐. + +Мне нравится читать различные технические статьи и книги 📘. +''' + +text.ua = ''' +Я інженер-програміст. + +Я розробляю різні ІТ-продукти, від embedded ⚙️ до веб-додатків 🌐 + +Мені подобається читати Різні технічні статті та книги 📘 ''' @@ -42,19 +65,22 @@ text.ru = ''' [page.gpg] title.en = "GPG Public Key" title.ru = "Публичный ключ GPG" -url = "/gpgkey.txt" -url_title = "thek4n.ru/gpgkey.txt" +title.ua = "Публічний ключ GPG" showkey_title.en = "Show key" showkey_title.ru = "Показать ключ" +showkey_title.ua = "Показати ключ" availat_title.en = "Available at" availat_title.ru = "Доступен по" +availat_title.ua = "Доступний по" -copy_title.en = "Show key" -copy_title.ru = "Показать ключ" +copy_title.en = "Copy key" +copy_title.ru = "Скопировать ключ" +copy_title.ua = "Скопіювати ключ" copied_title.en = "Copied!" copied_title.ru = "Скопировано!" +copied_title.ua = "Скопійовано!" value = ''' -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -116,6 +142,7 @@ Fs/cX/nWHveKYtN9iWsgHnYwm7hvg3A2k6JMwec3Yp0NmeJinfhJaihqxg== [page.projects] title.en = "Projects" title.ru = "Проекты" +title.ua = "Проекти" [[projects]] title = "ip.thek4n.ru" @@ -124,6 +151,7 @@ source_url = "https://gitea.thek4n.ru/thek4n/ip.thek4n.ru" source_url_title = "thek4n/ip.thek4n.ru" description.en = "check my ip service" description.ru = "сервис определения ip" +description.ua = "сервіс визначення ip" [[projects]] title = "paste.thek4n.ru" @@ -132,6 +160,7 @@ source_url = "https://gitea.thek4n.ru/thek4n/paste.thek4n.ru" source_url_title = "thek4n/paste.thek4n.ru" description.en = "copy/paste and url shortener service" description.ru = "укоротитель ссылок" +description.ua = "укоротитель посилань" [[projects]] title = "thek4n.ru" @@ -140,6 +169,7 @@ source_url = "https://gitea.thek4n.ru/thek4n/thek4n.ru" source_url_title = "thek4n/thek4n.ru" description.en = "this site source" description.ru = "исходники этого сайта" +description.ua = "джерела цього сайту" # --- @@ -148,21 +178,25 @@ description.ru = "исходники этого сайта" [page.contacts] title.en = "Contacts" title.ru = "Контакты" +title.ua = "Контакти" [[contacts]] site_name.en = "Telegram" site_name.ru = "Телеграм" +site_name.ua = "Телеграм" title = "@thek4n" url = "https://t.me/thek4n" [[contacts]] site_name.en = "Github" site_name.ru = "Github" +site_name.ua = "Github" title = "github.com/thek4n" url = "https://github.com/thek4n" [[contacts]] site_name.en = "Email" site_name.ru = "Почта" +site_name.ua = "Пошта" title = "thek4n@yandex.ru" url = "mailto:thek4n@yandex.ru" diff --git a/src/client/detector.rs b/src/client/detector.rs new file mode 100644 index 0000000..8096d18 --- /dev/null +++ b/src/client/detector.rs @@ -0,0 +1,63 @@ +use axum::http::HeaderMap; +use axum::http::header::USER_AGENT; + +#[derive(Debug, PartialEq)] +pub enum ClientType { + Browser, + CliLike, + Unknown, +} + +pub fn detect_client_type(headers: &HeaderMap) -> ClientType { + let user_agent = headers + .get(USER_AGENT) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + let user_agent_lower = user_agent.to_lowercase(); + + let accept = headers + .get("accept") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + let accept_lower = accept.to_lowercase(); + + let is_likely_cli = accept_lower.is_empty() + || accept_lower == "*/*" + || accept_lower == "application/json" + || accept_lower == "text/plain"; + + let is_likely_browser = accept_lower.contains("text/html") + && (accept_lower.contains("application/xhtml+xml") + || accept_lower.contains("application/xml") + || accept_lower.contains("*/*")); + + if user_agent_lower.contains("curl") + || user_agent_lower.contains("wget") + || user_agent_lower.contains("httpie") + || user_agent_lower.contains("python-requests") + || user_agent_lower.contains("go-http-client") + || (is_likely_cli && !user_agent_lower.contains("mozilla/")) + { + return ClientType::CliLike; + } + + if user_agent_lower.contains("mozilla/") + && (user_agent_lower.contains("chrome") + || user_agent_lower.contains("safari") + || user_agent_lower.contains("firefox") + || user_agent_lower.contains("edg")) + && is_likely_browser + { + return ClientType::Browser; + } + + if is_likely_browser { + ClientType::Browser + } else if is_likely_cli { + ClientType::CliLike + } else { + ClientType::Unknown + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..1fa1e42 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,4 @@ +mod detector; + +pub use detector::ClientType; +pub use detector::detect_client_type; diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..376bd67 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,12 @@ +mod models; + +pub use models::*; + +use std::fs; +use std::path::PathBuf; + +pub fn load_config(path: PathBuf) -> Result> { + let contents = fs::read_to_string(path)?; + let config: Config = toml::from_str(&contents)?; + Ok(config) +} diff --git a/src/config/models.rs b/src/config/models.rs new file mode 100644 index 0000000..e41253b --- /dev/null +++ b/src/config/models.rs @@ -0,0 +1,70 @@ +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, Deserialize, Clone)] +pub struct Config { + pub page: PageConfig, + pub languages: Vec, + pub projects: Option>, + pub contacts: Option>, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct PageConfig { + pub title: HashMap, + pub meta_title: String, + pub meta_description: String, + pub description: HashMap, + pub copyright_author: String, + pub aboutme: Option, + pub gpg: Option, + pub projects: Option, + pub contacts: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct AboutMeConfig { + pub title: HashMap, + pub text: HashMap, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct GPGConfig { + pub title: HashMap, + pub showkey_title: HashMap, + pub availat_title: HashMap, + pub copy_title: HashMap, + pub value: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ProjectsSectionConfig { + pub title: HashMap, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ContactsSectionConfig { + pub title: HashMap, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ProjectConfig { + pub title: String, + pub url: String, + pub source_url: String, + pub source_url_title: String, + pub description: HashMap, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ContactConfig { + pub site_name: HashMap, + pub title: String, + pub url: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct LanguageConfig { + pub key: String, + pub title: String, +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..59ec67a --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,3 @@ +mod root; + +pub use root::{gpg_handler, root_handler}; diff --git a/src/handlers/root.rs b/src/handlers/root.rs new file mode 100644 index 0000000..d8e04bf --- /dev/null +++ b/src/handlers/root.rs @@ -0,0 +1,105 @@ +use askama::Template; +use axum::{ + extract::{Query, State}, + http::{HeaderMap, Request}, + response::{Html, IntoResponse}, +}; +use chrono::Datelike; +use serde::Deserialize; + +use crate::client::ClientType; +use crate::client::detect_client_type; +use crate::config::load_config; +use crate::language::detect_user_language; +use crate::templates::{build_html_template, build_text_template}; + +use crate::AppState; + +#[derive(Debug, Deserialize)] +pub struct RootParams { + pub lang: Option, +} + +fn get_base_url(request: &Request) -> String { + let protocol = request + .headers() + .get("x-forwarded-proto") + .and_then(|p| p.to_str().ok()) + .or_else(|| request.uri().scheme_str()) + .unwrap_or("http"); + + let host = request + .headers() + .get("x-forwarded-host") + .and_then(|h| h.to_str().ok()) + .or_else(|| request.headers().get("host").and_then(|h| h.to_str().ok())) + .unwrap_or("unknown"); + + format!("{}://{}", protocol, host) +} + +pub async fn root_handler( + State(state): State, + headers: HeaderMap, + Query(params): Query, + request: Request, +) -> impl IntoResponse { + let config = match load_config(state.config_path.clone()) { + Ok(cfg) => cfg, + Err(e) => { + eprintln!("Failed to load config: {}", e); + return "Internal Server Error".into_response(); + } + }; + + let accept_language = headers + .get(axum::http::header::ACCEPT_LANGUAGE) + .and_then(|v| v.to_str().ok()); + + let lang = detect_user_language(params.lang, accept_language, &config.languages); + + let year = chrono::Utc::now().year().to_string(); + let base_url = get_base_url(&request); + + match detect_client_type(&headers) { + ClientType::Browser => { + match build_html_template( + &config, + &lang, + &base_url, + &year, + &state.css_url, + &state.script_js_url, + ) + .render() + { + Ok(html) => Html(html).into_response(), + Err(e) => { + eprintln!("Failed to render HTML template: {}", e); + "Internal Server Error".into_response() + } + } + } + ClientType::CliLike | ClientType::Unknown => { + let text = build_text_template(&config, &lang, &base_url, &year) + .render() + .unwrap_or_else(|e| { + eprintln!("Failed to render text template: {}", e); + "Internal Server Error".to_string() + }); + format!("{}\n", text).into_response() + } + } +} + +pub async fn gpg_handler(State(state): State) -> impl IntoResponse { + let config = match load_config(state.config_path.clone()) { + Ok(cfg) => cfg, + Err(e) => { + eprintln!("Failed to load config: {}", e); + return "Internal Server Error".into_response(); + } + }; + + config.page.gpg.unwrap().value.into_response() +} diff --git a/src/language/detector.rs b/src/language/detector.rs new file mode 100644 index 0000000..e308ed1 --- /dev/null +++ b/src/language/detector.rs @@ -0,0 +1,67 @@ +use crate::config::LanguageConfig; + +#[derive(Debug, PartialEq, Clone)] +struct LanguageWeight { + code: String, + weight: f32, +} + +pub fn detect_user_language( + query_lang: Option, + accept_language_header: Option<&str>, + supported_languages: &[LanguageConfig], +) -> String { + if let Some(lang) = query_lang { + if supported_languages.iter().any(|l| l.key == lang) { + return lang; + } + } + + if let Some(accept_lang) = accept_language_header { + let languages = parse_accept_language(accept_lang); + + for lang_weight in languages { + if supported_languages + .iter() + .any(|l| l.key == lang_weight.code) + { + return lang_weight.code; + } + } + } + + supported_languages + .first() + .map(|l| l.key.clone()) + .unwrap_or_else(|| "en".to_string()) +} + +fn parse_accept_language(header: &str) -> Vec { + let mut languages: Vec = Vec::new(); + + for part in header.split(',') { + let trimmed = part.trim(); + let parts: Vec<&str> = trimmed.split(';').collect(); + + let lang_code = parts[0]; + let main_lang = lang_code.split('-').next().unwrap_or(lang_code); + + let weight = if parts.len() > 1 { + parts[1] + .trim() + .strip_prefix("q=") + .and_then(|q| q.parse::().ok()) + .unwrap_or(1.0) + } else { + 1.0 + }; + + languages.push(LanguageWeight { + code: main_lang.to_string(), + weight, + }); + } + + languages.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap()); + languages +} diff --git a/src/language/mod.rs b/src/language/mod.rs new file mode 100644 index 0000000..0e0b742 --- /dev/null +++ b/src/language/mod.rs @@ -0,0 +1,3 @@ +mod detector; + +pub use detector::detect_user_language; diff --git a/src/main.rs b/src/main.rs index f8fc9d4..34d0e5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,477 +1,134 @@ -use askama::Template; -use axum::{ - Router, - extract::Query, - http::HeaderMap, - http::header::{ACCEPT, USER_AGENT}, - response::{Html, IntoResponse}, - routing::get, -}; -use chrono::Datelike; -use serde::Deserialize; -use std::collections::HashMap; -use std::fs; -use std::net::SocketAddr; +mod client; +mod config; +mod handlers; +mod language; +mod templates; -#[derive(Debug, Deserialize)] -struct Config { - page: PageConfig, - languages: Vec, - projects: Option>, - contacts: Option>, -} +use axum::Router; +use axum::extract::State; +use axum::response::IntoResponse; +use axum::routing::get; +use clap::Parser; +use handlers::{gpg_handler, root_handler}; +use sha2::{Digest, Sha256}; +use std::net::ToSocketAddrs; +use std::ops::RangeInclusive; -#[derive(Debug, Deserialize)] -struct PageConfig { - title: HashMap, // теперь HashMap - meta_title: String, - meta_description: String, - description: HashMap, // теперь HashMap - copyright_author: String, - aboutme: Option, - gpg: Option, - projects: Option, - contacts: Option, -} +const STATIC_JS: &'static str = include_str!("../assets/script.js"); +const STATIC_CSS: &'static str = include_str!("../assets/style.css"); -#[derive(Debug, Deserialize)] -struct AboutMeConfig { - title: HashMap, - text: HashMap, -} +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Path to configuration file + #[arg(short, long)] + config: std::path::PathBuf, -#[derive(Debug, Deserialize)] -struct GPGConfig { - title: HashMap, - url: String, - url_title: String, - showkey_title: HashMap, - availat_title: HashMap, - copy_title: HashMap, - value: String, -} + /// Network port to use + #[arg(short, long, value_parser = port_in_range, value_name = "PORT", default_value = "8080")] + port: u16, -#[derive(Debug, Deserialize)] -struct ProjectsSectionConfig { - title: HashMap, -} - -#[derive(Debug, Deserialize)] -struct ContactsSectionConfig { - title: HashMap, -} - -#[derive(Debug, Deserialize)] -struct ProjectConfig { - title: String, - url: String, - source_url: String, - source_url_title: String, - description: HashMap, -} - -#[derive(Debug, Deserialize)] -struct ContactConfig { - site_name: HashMap, - title: String, - url: String, -} - -#[derive(Debug, Deserialize)] -struct LanguageConfig { - key: String, - title: String, -} - -// Template structures matching the HTML -#[derive(Template)] -#[template(path = "html_index.jinja2")] -struct HtmlPageTemplate<'a> { - title: &'a str, - description: &'a str, - copyright_author: &'a str, - base_url: &'a str, - meta: PageMeta<'a>, - year: &'a str, - aboutme: AboutMeSection<'a>, - gpg_section: GPGSection<'a>, - contacts_section: ContactsSection<'a>, - projects_section: ProjectsSection<'a>, - languages: Vec>, -} - -#[derive(Template)] -#[template(path = "text_index.jinja2", escape = "none")] -struct TextPageTemplate<'a> { - title: &'a str, - description: &'a str, - copyright_author: &'a str, - base_url: &'a str, - year: &'a str, - aboutme_section: AboutMeSection<'a>, - gpg_section: GPGSection<'a>, - contacts_section: ContactsSection<'a>, - projects_section: ProjectsSection<'a>, -} - -#[derive(Clone, Copy)] -struct PageMeta<'a> { - title: &'a str, - description: &'a str, -} - -#[derive(Clone, Copy)] -struct AboutMeSection<'a> { - title: &'a str, - text: &'a str, -} - -#[derive(Clone, Copy)] -struct GPGSection<'a> { - title: &'a str, - availat_title: &'a str, - url: &'a str, - url_title: &'a str, - showkey_title: &'a str, - copy_title: &'a str, - value: &'a str, + /// Host to listen + #[arg(long, default_value = "127.0.0.1")] + host: String, } #[derive(Clone)] -struct ContactsSection<'a> { - title: &'a str, - contacts: Vec>, +pub struct AppState { + config_path: std::path::PathBuf, + css_content: &'static str, + js_content: &'static str, + css_url: String, + script_js_url: String, } -#[derive(Clone)] -struct Contact<'a> { - site_name: &'a str, - url: &'a str, - title: &'a str, -} +const PORT_RANGE: RangeInclusive = 1..=65535; -#[derive(Clone)] -struct ProjectsSection<'a> { - title: &'a str, - projects: Vec>, -} +fn port_in_range(s: &str) -> Result { + let port: usize = s + .parse() + .map_err(|_| format!("`{s}` isn't a port number"))?; -#[derive(Clone)] -struct Project<'a> { - title: &'a str, - url: &'a str, - source_url: &'a str, - source_url_title: &'a str, - description: &'a str, -} - -#[derive(Clone)] -struct Language<'a> { - key: &'a str, - current: bool, - value: &'a str, -} - -fn load_config(path: &str) -> Result> { - let contents = fs::read_to_string(path)?; - let config: Config = toml::from_str(&contents)?; - Ok(config) -} - -fn build_aboutme_section<'a>(config: &'a Config, lang_key: &'a str) -> AboutMeSection<'a> { - AboutMeSection { - title: config - .page - .aboutme - .as_ref() - .and_then(|a| a.title.get(lang_key)) - .map(|s| s.as_str()) - .unwrap_or(""), - text: config - .page - .aboutme - .as_ref() - .and_then(|a| a.text.get(lang_key)) - .map(|s| s.as_str()) - .unwrap_or(""), - } -} - -fn build_gpg_section<'a>(config: &'a Config, lang_key: &'a str) -> GPGSection<'a> { - let gpg_config = config.page.gpg.as_ref().unwrap(); - GPGSection { - title: gpg_config - .title - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - availat_title: gpg_config - .availat_title - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - url: &gpg_config.url, - url_title: &gpg_config.url_title, - showkey_title: gpg_config - .showkey_title - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - copy_title: gpg_config - .copy_title - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - value: &gpg_config.value, - } -} - -fn build_contacts_section<'a>(config: &'a Config, lang_key: &'a str) -> ContactsSection<'a> { - let contacts_config = config.page.contacts.as_ref().unwrap(); - let contacts: Vec> = config - .contacts - .as_ref() - .unwrap() - .iter() - .map(|contact| Contact { - site_name: contact - .site_name - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - url: &contact.url, - title: &contact.title, - }) - .collect(); - - ContactsSection { - title: contacts_config - .title - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - contacts, - } -} - -fn build_projects_section<'a>(config: &'a Config, lang_key: &'a str) -> ProjectsSection<'a> { - let projects_config = config.page.projects.as_ref().unwrap(); - let projects: Vec> = config - .projects - .as_ref() - .unwrap() - .iter() - .map(|project| Project { - title: &project.title, - url: &project.url, - source_url: &project.source_url, - source_url_title: &project.source_url_title, - description: project - .description - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - }) - .collect(); - - ProjectsSection { - title: projects_config - .title - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - projects, - } -} - -fn build_languages<'a>(config: &'a Config, lang_key: &'a str) -> Vec> { - config - .languages - .iter() - .map(|lang| Language { - key: &lang.key, - current: lang.key == lang_key, - value: &lang.title, - }) - .collect() -} - -fn build_html_template<'a>( - config: &'a Config, - lang_key: &'a str, - base_url: &'a str, - year: &'a str, -) -> HtmlPageTemplate<'a> { - let meta = PageMeta { - title: &config.page.meta_title, - description: &config.page.meta_description, - }; - - HtmlPageTemplate { - title: config - .page - .title - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - description: config - .page - .description - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - copyright_author: &config.page.copyright_author, - base_url, - meta, - year, - aboutme: build_aboutme_section(config, lang_key), - gpg_section: build_gpg_section(config, lang_key), - contacts_section: build_contacts_section(config, lang_key), - projects_section: build_projects_section(config, lang_key), - languages: build_languages(config, lang_key), - } -} - -fn build_text_template<'a>( - config: &'a Config, - lang_key: &'a str, - base_url: &'a str, - year: &'a str, -) -> TextPageTemplate<'a> { - TextPageTemplate { - title: config - .page - .title - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - description: config - .page - .description - .get(lang_key) - .map(|s| s.as_str()) - .unwrap_or(""), - copyright_author: &config.page.copyright_author, - base_url, - year, - aboutme_section: build_aboutme_section(config, lang_key), - gpg_section: build_gpg_section(config, lang_key), - contacts_section: build_contacts_section(config, lang_key), - projects_section: build_projects_section(config, lang_key), - } -} - -#[derive(Debug, PartialEq)] -enum ClientType { - Browser, - CliLike, // curl, wget, httpie и т.д. - Unknown, -} - -fn detect_client_type(headers: &HeaderMap) -> ClientType { - let user_agent = headers - .get(USER_AGENT) - .and_then(|v| v.to_str().ok()) - .unwrap_or(""); - - let user_agent_lower = user_agent.to_lowercase(); - - // Проверка Accept header - let accept = headers - .get(ACCEPT) - .and_then(|v| v.to_str().ok()) - .unwrap_or(""); - - let accept_lower = accept.to_lowercase(); - - // CLI утилиты обычно имеют простой Accept или не имеют его вовсе - let is_likely_cli = accept_lower.is_empty() - || accept_lower == "*/*" - || accept_lower == "application/json" - || accept_lower == "text/plain"; - - // Браузеры всегда запрашивают text/html в первую очередь - let is_likely_browser = accept_lower.contains("text/html") - && (accept_lower.contains("application/xhtml+xml") - || accept_lower.contains("application/xml") - || accept_lower.contains("*/*")); - - // Комбинированная проверка: User-Agent + Accept - if user_agent_lower.contains("curl") - || user_agent_lower.contains("wget") - || user_agent_lower.contains("httpie") - || user_agent_lower.contains("python-requests") - || user_agent_lower.contains("go-http-client") - || (is_likely_cli && !user_agent_lower.contains("mozilla/")) - { - return ClientType::CliLike; - } - - if user_agent_lower.contains("mozilla/") - && (user_agent_lower.contains("chrome") - || user_agent_lower.contains("safari") - || user_agent_lower.contains("firefox") - || user_agent_lower.contains("edg")) - && is_likely_browser - { - return ClientType::Browser; - } - - // Fallback с использованием только Accept header - if is_likely_browser { - ClientType::Browser - } else if is_likely_cli { - ClientType::CliLike + if PORT_RANGE.contains(&port) { + Ok(port as u16) } else { - ClientType::Unknown - } -} - -#[derive(Debug, Deserialize)] -pub struct RootParams { - pub lang: Option, -} - -async fn root_handler(headers: HeaderMap, Query(params): Query) -> impl IntoResponse { - let query_lang = params.lang.unwrap_or("en".to_string()); - - let config = load_config("docs/config_example.toml").unwrap(); - - let lang_exists = config - .languages - .iter() - .any(|lang_config| lang_config.key == query_lang); - - let lang = if lang_exists { - query_lang.as_str() - } else { - "en" - }; - - let year = chrono::Utc::now().year().to_string(); - - match detect_client_type(&headers) { - ClientType::Browser => Html( - build_html_template(&config, lang, "https://thek4n.ru", year.as_str()) - .render() - .unwrap(), - ) - .into_response(), - ClientType::CliLike | ClientType::Unknown => format!( - "{}\n", - build_text_template(&config, lang, "https://thek4n.ru", year.as_str()) - ) - .into_response(), + Err(format!( + "port not in range {}-{}", + PORT_RANGE.start(), + PORT_RANGE.end() + )) } } #[tokio::main] async fn main() { - let app = Router::new().route("/", get(root_handler)); + let args = Args::parse(); - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + let css_hash = calculate_sha256(STATIC_CSS); + let js_hash = calculate_sha256(STATIC_JS); - axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) - .await - .unwrap(); + let css_path = format!("/style-{css_hash}.js"); + let js_path = format!("/script-{js_hash}.js"); + + let state = AppState { + config_path: args.config, + css_content: STATIC_CSS, + js_content: STATIC_JS, + css_url: css_path.clone(), + script_js_url: js_path.clone(), + }; + + let app = Router::new() + .route("/", get(root_handler)) + .route("/gpgkey.txt", get(gpg_handler)) + .route(css_path.as_str(), get(static_css_handler)) + .route(js_path.as_str(), get(static_js_handler)) + .with_state(state); + + let addr = format!("{}:{}", args.host, args.port) + .to_socket_addrs() + .expect("Fail to get address") + .next() + .ok_or("failed to resolve address") + .expect(""); + + println!("Server running on http://{}", addr); + + axum::serve( + tokio::net::TcpListener::bind(addr) + .await + .expect("Failed to bind to address"), + app, + ) + .await + .expect("Server failed to start"); +} + +async fn static_css_handler(State(state): State) -> impl IntoResponse { + let headers = [ + (axum::http::header::CONTENT_TYPE, mime::CSS.as_ref()), + (axum::http::header::CACHE_CONTROL, "public, max-age=86400"), + ]; + + (headers, state.css_content).into_response() +} + +async fn static_js_handler(State(state): State) -> impl IntoResponse { + let headers = [ + ( + axum::http::header::CONTENT_TYPE, + mime::APPLICATION_JAVASCRIPT.as_ref(), + ), + (axum::http::header::CACHE_CONTROL, "public, max-age=86400"), + ]; + + (headers, state.js_content).into_response() +} + +fn calculate_sha256(input: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + + hex::encode(result)[..6].to_string() } diff --git a/src/templates/builders.rs b/src/templates/builders.rs new file mode 100644 index 0000000..20cb821 --- /dev/null +++ b/src/templates/builders.rs @@ -0,0 +1,236 @@ +use crate::config::Config; +use askama::Template; +use std::collections::HashMap; + +#[derive(Template)] +#[template(path = "html_index.jinja2")] +pub struct HtmlPageTemplate<'a> { + pub title: &'a str, + pub description: &'a str, + pub copyright_author: &'a str, + pub base_url: &'a str, + pub meta: PageMeta<'a>, + pub year: &'a str, + pub aboutme: AboutMeSection<'a>, + pub gpg_section: GPGSection<'a>, + pub contacts_section: ContactsSection<'a>, + pub projects_section: ProjectsSection<'a>, + pub languages: Vec>, +} + +#[derive(Template)] +#[template(path = "text_index.jinja2", escape = "none")] +pub struct TextPageTemplate<'a> { + pub title: &'a str, + pub description: &'a str, + pub copyright_author: &'a str, + pub base_url: &'a str, + pub year: &'a str, + pub aboutme_section: AboutMeSection<'a>, + pub gpg_section: GPGSection<'a>, + pub contacts_section: ContactsSection<'a>, + pub projects_section: ProjectsSection<'a>, +} + +#[derive(Clone, Copy)] +pub struct PageMeta<'a> { + pub title: &'a str, + pub description: &'a str, + pub css_url: &'a str, + pub script_js_url: &'a str, +} + +#[derive(Clone, Copy)] +pub struct AboutMeSection<'a> { + pub title: &'a str, + pub text: &'a str, +} + +#[derive(Clone, Copy)] +pub struct GPGSection<'a> { + pub title: &'a str, + pub availat_title: &'a str, + pub showkey_title: &'a str, + pub copy_title: &'a str, + pub value: &'a str, +} + +#[derive(Clone)] +pub struct ContactsSection<'a> { + pub title: &'a str, + pub contacts: Vec>, +} + +#[derive(Clone)] +pub struct Contact<'a> { + pub site_name: &'a str, + pub url: &'a str, + pub title: &'a str, +} + +#[derive(Clone)] +pub struct ProjectsSection<'a> { + pub title: &'a str, + pub projects: Vec>, +} + +#[derive(Clone)] +pub struct Project<'a> { + pub title: &'a str, + pub url: &'a str, + pub source_url: &'a str, + pub source_url_title: &'a str, + pub description: &'a str, +} + +#[derive(Clone)] +pub struct Language<'a> { + pub key: &'a str, + pub current: bool, + pub value: &'a str, +} + +// Вспомогательная функция для безопасного получения значения из HashMap +fn get_localized_string<'a>(map: &'a HashMap, key: &str) -> &'a str { + map.get(key).map(|s| s.as_str()).unwrap_or("") +} + +pub fn build_html_template<'a>( + config: &'a Config, + lang_key: &'a str, + base_url: &'a str, + year: &'a str, + css_url: &'a str, + script_js_url: &'a str, +) -> HtmlPageTemplate<'a> { + let meta = PageMeta { + title: &config.page.meta_title, + description: &config.page.meta_description, + css_url, + script_js_url, + }; + + HtmlPageTemplate { + title: get_localized_string(&config.page.title, lang_key), + description: get_localized_string(&config.page.description, lang_key), + copyright_author: &config.page.copyright_author, + base_url, + meta, + year, + aboutme: build_aboutme_section(config, lang_key), + gpg_section: build_gpg_section(config, lang_key), + contacts_section: build_contacts_section(config, lang_key), + projects_section: build_projects_section(config, lang_key), + languages: build_languages(config, lang_key), + } +} + +pub fn build_text_template<'a>( + config: &'a Config, + lang_key: &'a str, + base_url: &'a str, + year: &'a str, +) -> TextPageTemplate<'a> { + TextPageTemplate { + title: get_localized_string(&config.page.title, lang_key), + description: get_localized_string(&config.page.description, lang_key), + copyright_author: &config.page.copyright_author, + base_url, + year, + aboutme_section: build_aboutme_section(config, lang_key), + gpg_section: build_gpg_section(config, lang_key), + contacts_section: build_contacts_section(config, lang_key), + projects_section: build_projects_section(config, lang_key), + } +} + +fn build_aboutme_section<'a>(config: &'a Config, lang_key: &'a str) -> AboutMeSection<'a> { + AboutMeSection { + title: config + .page + .aboutme + .as_ref() + .map(|a| get_localized_string(&a.title, lang_key)) + .unwrap_or(""), + text: config + .page + .aboutme + .as_ref() + .map(|a| get_localized_string(&a.text, lang_key)) + .unwrap_or(""), + } +} + +fn build_gpg_section<'a>(config: &'a Config, lang_key: &'a str) -> GPGSection<'a> { + let gpg_config = config.page.gpg.as_ref().expect("GPG config is required"); + + GPGSection { + title: get_localized_string(&gpg_config.title, lang_key), + availat_title: get_localized_string(&gpg_config.availat_title, lang_key), + showkey_title: get_localized_string(&gpg_config.showkey_title, lang_key), + copy_title: get_localized_string(&gpg_config.copy_title, lang_key), + value: &gpg_config.value, + } +} + +fn build_contacts_section<'a>(config: &'a Config, lang_key: &'a str) -> ContactsSection<'a> { + let contacts_config = config + .page + .contacts + .as_ref() + .expect("Contacts config is required"); + let contacts = config + .contacts + .as_ref() + .expect("Contacts list is required") + .iter() + .map(|contact| Contact { + site_name: get_localized_string(&contact.site_name, lang_key), + url: &contact.url, + title: &contact.title, + }) + .collect(); + + ContactsSection { + title: get_localized_string(&contacts_config.title, lang_key), + contacts, + } +} + +fn build_projects_section<'a>(config: &'a Config, lang_key: &'a str) -> ProjectsSection<'a> { + let projects_config = config + .page + .projects + .as_ref() + .expect("Projects config is required"); + let projects = config + .projects + .as_ref() + .expect("Projects list is required") + .iter() + .map(|project| Project { + title: &project.title, + url: &project.url, + source_url: &project.source_url, + source_url_title: &project.source_url_title, + description: get_localized_string(&project.description, lang_key), + }) + .collect(); + + ProjectsSection { + title: get_localized_string(&projects_config.title, lang_key), + projects, + } +} + +fn build_languages<'a>(config: &'a Config, lang_key: &'a str) -> Vec> { + config + .languages + .iter() + .map(|lang| Language { + key: &lang.key, + current: lang.key == lang_key, + value: &lang.title, + }) + .collect() +} diff --git a/src/templates/mod.rs b/src/templates/mod.rs new file mode 100644 index 0000000..aaba874 --- /dev/null +++ b/src/templates/mod.rs @@ -0,0 +1,3 @@ +mod builders; + +pub use builders::{build_html_template, build_text_template}; diff --git a/templates/html_index.jinja2 b/templates/html_index.jinja2 index a7628b7..232a830 100644 --- a/templates/html_index.jinja2 +++ b/templates/html_index.jinja2 @@ -6,7 +6,7 @@ {{ meta.title }} - +
@@ -39,28 +39,6 @@

{{ aboutme.text }}

-
-

{{ gpg_section.title }}

- -

- {{ gpg_section.availat_title }} - {{ gpg_section.url_title }} -

- -
- - - {{ gpg_section.showkey_title }} - {{ gpg_section.copy_title }} - - - {{ gpg_section.value|linebreaksbr }} - - -
- -
-

{{ contacts_section.title }}

{% for contact in contacts_section.contacts %} @@ -72,11 +50,30 @@ {% endfor %}
+
+

{{ gpg_section.title }}

+ +

+ {{ gpg_section.availat_title }} + {{ base_url }}/gpgkey.txt +

+ +
+ + + {{ gpg_section.showkey_title }} + {{ gpg_section.copy_title }} + + +
{{ gpg_section.value|linebreaksbr }}
+
+
+

© {{ year }} {{ copyright_author }}

- + diff --git a/templates/text_index.jinja2 b/templates/text_index.jinja2 index 19f8db5..802fff7 100644 --- a/templates/text_index.jinja2 +++ b/templates/text_index.jinja2 @@ -7,18 +7,16 @@ {%- endfor %} +{{ aboutme_section.title }}: +{{ aboutme_section.text }} + + {{ contacts_section.title }}: {%- for contact in contacts_section.contacts %} - > {{ contact.site_name }}: {{ contact.title }} + - {{ contact.site_name }}: {{ contact.title }} {%- endfor %} - -{{ gpg_section.title }}: {{ base_url }}{{ gpg_section.url }} - - -{{ aboutme_section.title }}: - {{ aboutme_section.text }} - +{{ gpg_section.title }}: {{ base_url }}/gpgkey.txt © {{ year }} {{ copyright_author }}