Compare commits
3 Commits
d8da31f8b6
...
212e3abb86
| Author | SHA1 | Date | |
|---|---|---|---|
| 212e3abb86 | |||
| 070953f477 | |||
| e1fca0eb1e |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
||||
target/
|
||||
.git/
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
README.md
|
||||
.env
|
||||
*.md
|
||||
217
Cargo.lock
generated
217
Cargo.lock
generated
@ -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"
|
||||
|
||||
13
Cargo.toml
13
Cargo.toml
@ -1,10 +1,16 @@
|
||||
[package]
|
||||
name = "thek4n-ru"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
||||
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 +18,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"
|
||||
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@ -0,0 +1,37 @@
|
||||
# Chef
|
||||
FROM rust:1.94 AS chef
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends musl-tools && rm -rf /var/lib/apt/lists/*
|
||||
RUN rustup target add x86_64-unknown-linux-musl
|
||||
RUN cargo install --locked cargo-chef
|
||||
WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock .
|
||||
RUN mkdir src && touch src/main.rs
|
||||
# Если у вас workspace или несколько крейтов, скопируйте их манифесты тоже:
|
||||
# COPY crates/*/Cargo.toml ./crates/*/
|
||||
|
||||
RUN cargo chef prepare --recipe-path /app/recipe.json
|
||||
|
||||
|
||||
# Builder
|
||||
FROM chef AS builder
|
||||
|
||||
COPY --from=chef /app/recipe.json /app/recipe.json
|
||||
RUN cargo chef cook --target x86_64-unknown-linux-musl --release --recipe-path /app/recipe.json
|
||||
|
||||
# rust-toolchain должен содержать
|
||||
## targets = ["x86_64-unknown-linux-musl"]
|
||||
COPY rust-toolchain.toml* ./
|
||||
RUN cargo fetch
|
||||
|
||||
COPY . .
|
||||
RUN cargo build --target x86_64-unknown-linux-musl --release
|
||||
|
||||
|
||||
# Runtime
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/thek4n-ru /usr/local/bin/thek4n-ru
|
||||
|
||||
CMD ["/usr/local/bin/thek4n-ru"]
|
||||
18
TODO.md
18
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] Вынести конфиг в параметры
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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 <thek4n@yandex.ru>"
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
82
index.html
82
index.html
@ -1,82 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>thek4n</title>
|
||||
<meta name="description" content="thek4n - developer">
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="top-controls">
|
||||
<div>
|
||||
|
||||
<a id="lang-en" class="active" href="https://thek4n.ru/en">English</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h1>TheK4n</h1>
|
||||
<p><small>Developer</small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1>Projects</h1>
|
||||
|
||||
<p>
|
||||
<span><a href="https://paste.thek4n.ru">project</a></span>
|
||||
:
|
||||
<span><a href="https://gitea.thek4n.ru/thek4n/paste.thek4n.ru">thek4n/paste.thek4n.ru</a></span>
|
||||
<span><small class="muted">(copy/paste service)</small>
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1>About Me</h1>
|
||||
<p>About me text</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>GPG</h2>
|
||||
|
||||
<p>
|
||||
<span>Available at</span>
|
||||
<a href="/gpgkey.txt" target="_blank" rel="noopener noreferrer" >thek4n.ru/gpgkey.txt</a>
|
||||
</p>
|
||||
|
||||
<details class="gpg-details">
|
||||
<summary>
|
||||
::marker
|
||||
<span class="summary-row">
|
||||
Show key
|
||||
<a id="copy-gpg" class="copy-link" href="#" onclick="copyGPGKey()">Copy</a>
|
||||
</span>
|
||||
key....
|
||||
</summary>
|
||||
</details>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Contacts</h2>
|
||||
|
||||
<p>
|
||||
<span>Telegram</span>
|
||||
:
|
||||
<span><a href="https://t.me/thek4n">@thek4n</a></span>
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>© 2026 Vladislav Kan <thek4n@yandex.ru></p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,3 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
components = ["clippy", "rustfmt"]
|
||||
targets = ["x86_64-unknown-linux-musl"]
|
||||
|
||||
63
src/client/detector.rs
Normal file
63
src/client/detector.rs
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
4
src/client/mod.rs
Normal file
4
src/client/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod detector;
|
||||
|
||||
pub use detector::ClientType;
|
||||
pub use detector::detect_client_type;
|
||||
12
src/config/mod.rs
Normal file
12
src/config/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
mod models;
|
||||
|
||||
pub use models::*;
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn load_config(path: PathBuf) -> Result<Config, Box<dyn std::error::Error>> {
|
||||
let contents = fs::read_to_string(path)?;
|
||||
let config: Config = toml::from_str(&contents)?;
|
||||
Ok(config)
|
||||
}
|
||||
70
src/config/models.rs
Normal file
70
src/config/models.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
pub page: PageConfig,
|
||||
pub languages: Vec<LanguageConfig>,
|
||||
pub projects: Option<Vec<ProjectConfig>>,
|
||||
pub contacts: Option<Vec<ContactConfig>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct PageConfig {
|
||||
pub title: HashMap<String, String>,
|
||||
pub meta_title: String,
|
||||
pub meta_description: String,
|
||||
pub description: HashMap<String, String>,
|
||||
pub copyright_author: String,
|
||||
pub aboutme: Option<AboutMeConfig>,
|
||||
pub gpg: Option<GPGConfig>,
|
||||
pub projects: Option<ProjectsSectionConfig>,
|
||||
pub contacts: Option<ContactsSectionConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct AboutMeConfig {
|
||||
pub title: HashMap<String, String>,
|
||||
pub text: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct GPGConfig {
|
||||
pub title: HashMap<String, String>,
|
||||
pub showkey_title: HashMap<String, String>,
|
||||
pub availat_title: HashMap<String, String>,
|
||||
pub copy_title: HashMap<String, String>,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ProjectsSectionConfig {
|
||||
pub title: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ContactsSectionConfig {
|
||||
pub title: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[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<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ContactConfig {
|
||||
pub site_name: HashMap<String, String>,
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct LanguageConfig {
|
||||
pub key: String,
|
||||
pub title: String,
|
||||
}
|
||||
3
src/handlers/mod.rs
Normal file
3
src/handlers/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod root;
|
||||
|
||||
pub use root::{gpg_handler, root_handler};
|
||||
105
src/handlers/root.rs
Normal file
105
src/handlers/root.rs
Normal file
@ -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<String>,
|
||||
}
|
||||
|
||||
fn get_base_url(request: &Request<axum::body::Body>) -> 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<AppState>,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<RootParams>,
|
||||
request: Request<axum::body::Body>,
|
||||
) -> 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<AppState>) -> 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()
|
||||
}
|
||||
67
src/language/detector.rs
Normal file
67
src/language/detector.rs
Normal file
@ -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<String>,
|
||||
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<LanguageWeight> {
|
||||
let mut languages: Vec<LanguageWeight> = 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::<f32>().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
|
||||
}
|
||||
3
src/language/mod.rs
Normal file
3
src/language/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod detector;
|
||||
|
||||
pub use detector::detect_user_language;
|
||||
567
src/main.rs
567
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<LanguageConfig>,
|
||||
projects: Option<Vec<ProjectConfig>>,
|
||||
contacts: Option<Vec<ContactConfig>>,
|
||||
}
|
||||
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<String, String>, // теперь HashMap
|
||||
meta_title: String,
|
||||
meta_description: String,
|
||||
description: HashMap<String, String>, // теперь HashMap
|
||||
copyright_author: String,
|
||||
aboutme: Option<AboutMeConfig>,
|
||||
gpg: Option<GPGConfig>,
|
||||
projects: Option<ProjectsSectionConfig>,
|
||||
contacts: Option<ContactsSectionConfig>,
|
||||
}
|
||||
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<String, String>,
|
||||
text: HashMap<String, String>,
|
||||
}
|
||||
#[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<String, String>,
|
||||
url: String,
|
||||
url_title: String,
|
||||
showkey_title: HashMap<String, String>,
|
||||
availat_title: HashMap<String, String>,
|
||||
copy_title: HashMap<String, String>,
|
||||
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<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ContactsSectionConfig {
|
||||
title: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ProjectConfig {
|
||||
title: String,
|
||||
url: String,
|
||||
source_url: String,
|
||||
source_url_title: String,
|
||||
description: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ContactConfig {
|
||||
site_name: HashMap<String, String>,
|
||||
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<Language<'a>>,
|
||||
}
|
||||
|
||||
#[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<Contact<'a>>,
|
||||
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<usize> = 1..=65535;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ProjectsSection<'a> {
|
||||
title: &'a str,
|
||||
projects: Vec<Project<'a>>,
|
||||
}
|
||||
fn port_in_range(s: &str) -> Result<u16, String> {
|
||||
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<Config, Box<dyn std::error::Error>> {
|
||||
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<Contact<'a>> = 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<Project<'a>> = 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<Language<'a>> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
async fn root_handler(headers: HeaderMap, Query(params): Query<RootParams>) -> 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)
|
||||
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
|
||||
.unwrap();
|
||||
.expect("Failed to bind to address"),
|
||||
app,
|
||||
)
|
||||
.await
|
||||
.expect("Server failed to start");
|
||||
}
|
||||
|
||||
async fn static_css_handler(State(state): State<AppState>) -> 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<AppState>) -> 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()
|
||||
}
|
||||
|
||||
236
src/templates/builders.rs
Normal file
236
src/templates/builders.rs
Normal file
@ -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<Language<'a>>,
|
||||
}
|
||||
|
||||
#[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<Contact<'a>>,
|
||||
}
|
||||
|
||||
#[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<Project<'a>>,
|
||||
}
|
||||
|
||||
#[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<String, String>, 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<Language<'a>> {
|
||||
config
|
||||
.languages
|
||||
.iter()
|
||||
.map(|lang| Language {
|
||||
key: &lang.key,
|
||||
current: lang.key == lang_key,
|
||||
value: &lang.title,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
3
src/templates/mod.rs
Normal file
3
src/templates/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod builders;
|
||||
|
||||
pub use builders::{build_html_template, build_text_template};
|
||||
@ -6,7 +6,7 @@
|
||||
<title>{{ meta.title }}</title>
|
||||
<meta name="description" content="{{ meta.description }}">
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="{{ meta.css_url }}">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
@ -39,28 +39,6 @@
|
||||
<p>{{ aboutme.text }}</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>{{ gpg_section.title }}</h2>
|
||||
|
||||
<p>
|
||||
<span>{{ gpg_section.availat_title }}</span>
|
||||
<a href="{{ gpg_section.url }}" target="_blank" rel="noopener noreferrer" >{{ gpg_section.url_title }}</a>
|
||||
</p>
|
||||
|
||||
<details class="gpg-details">
|
||||
<summary>
|
||||
<span class="summary-row">
|
||||
{{ gpg_section.showkey_title }}
|
||||
<a id="copy-gpg" class="copy-link" href="#" onclick="copyGPGKey()">{{ gpg_section.copy_title }}</a>
|
||||
</span>
|
||||
<code>
|
||||
{{ gpg_section.value|linebreaksbr }}
|
||||
</code>
|
||||
</summary>
|
||||
</details>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>{{ contacts_section.title }}</h2>
|
||||
{% for contact in contacts_section.contacts %}
|
||||
@ -72,11 +50,30 @@
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>{{ gpg_section.title }}</h2>
|
||||
|
||||
<p>
|
||||
<span>{{ gpg_section.availat_title }}</span>
|
||||
<a href="{{ base_url }}/gpgkey.txt" target="_blank" rel="noopener noreferrer" >{{ base_url }}/gpgkey.txt</a>
|
||||
</p>
|
||||
|
||||
<details class="gpg-details">
|
||||
<summary>
|
||||
<span class="summary-row">
|
||||
{{ gpg_section.showkey_title }}
|
||||
<a id="copy-gpg" class="copy-link" href="#" onclick="copyGPGKey()">{{ gpg_section.copy_title }}</a>
|
||||
</span>
|
||||
</summary>
|
||||
<pre id="gpg-key">{{ gpg_section.value|linebreaksbr }}</pre>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>© {{ year }} {{ copyright_author }}</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script src="script.js"></script>
|
||||
<script src="{{ meta.script_js_url }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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 }}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user