feat: markdown rendering /m/<id>
This commit is contained in:
268
.llm/analysis.md
Normal file
268
.llm/analysis.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# Analiza repozytorium: bin (Pastebin)
|
||||
|
||||
> Data analizy: 2026-04-21
|
||||
|
||||
---
|
||||
|
||||
## 1. Cel projektu
|
||||
|
||||
**Bin** to minimalistyczny, samodzielny serwis pastebin akceptujący zarówno tekst, jak i pliki binarne (obrazy, PDF-y itp.). Filozofia projektu opiera się na prostocie, łatwości wdrożenia i minimalizmie. W odróżnieniu od tradycyjnych pastebinów **nie wymaga bazy danych** — wszystkie pasty przechowywane są w płaskim systemie plików. Projekt zawiera klienty: interfejs webowy, CLI i integrację z Vimem.
|
||||
|
||||
**Live demo:** https://basedbin.fly.dev
|
||||
**Docker:** `wantguns/bin` (multi-arch: amd64 + arm64)
|
||||
|
||||
---
|
||||
|
||||
## 2. Stack technologiczny
|
||||
|
||||
| Warstwa | Technologia |
|
||||
|---------|-------------|
|
||||
| Język | Rust (Edition 2021) |
|
||||
| Framework web | Rocket 0.5.0-rc.1 (async) |
|
||||
| Szablony HTML | Tera (via rocket_dyn_templates) |
|
||||
| Kolorowanie składni | Syntect 4.6.0 + motyw Ayu Dark |
|
||||
| Frontend | Vanilla JavaScript, HTML, CSS (brak frameworków) |
|
||||
| Detekcja MIME | tree_magic 0.2.3 |
|
||||
| CLI args | clap 3.0.9 |
|
||||
| Kryptografia | sha256 (ETag) |
|
||||
| Obsadzanie zasobów | rust-embed 6.3.0 |
|
||||
| Konteneryzacja | Docker (obraz scratch) |
|
||||
| Deployment | Fly.io |
|
||||
|
||||
---
|
||||
|
||||
## 3. Struktura katalogów
|
||||
|
||||
```
|
||||
bin/
|
||||
├── src/ # Kod źródłowy Rust
|
||||
│ ├── main.rs # Punkt wejścia, setup serwera, parsowanie CLI
|
||||
│ ├── routes/ # Handlery endpointów HTTP
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── index.rs # GET / – strona główna
|
||||
│ │ ├── upload.rs # POST / – upload pliku binarnego
|
||||
│ │ ├── submit.rs # POST /submit – formularz tekstowy
|
||||
│ │ ├── retrieve.rs # GET /<id> – surowe pobranie pasty
|
||||
│ │ ├── pretty_retrieve.rs # GET /p/<id> – wyświetlanie z podświetlaniem
|
||||
│ │ └── static_files.rs # GET /static/<file> – zasoby statyczne
|
||||
│ └── models/
|
||||
│ ├── mod.rs
|
||||
│ ├── paste_id.rs # Generowanie i walidacja ID pasty
|
||||
│ ├── pretty_syntax.rs # Parsowanie rozszerzenia z URL
|
||||
│ ├── pretty.rs # Logika podświetlania składni
|
||||
│ └── response_wrapper.rs # Abstrakcja odpowiedzi HTTP
|
||||
│
|
||||
├── templates/ # Szablony Tera
|
||||
│ ├── base.html.tera
|
||||
│ ├── index.html.tera
|
||||
│ └── pretty.html.tera
|
||||
│
|
||||
├── static/ # Zasoby osadzone w binarce
|
||||
│ ├── css/
|
||||
│ ├── js/
|
||||
│ ├── fonts/ # Iosevka (ttf + woff2)
|
||||
│ └── media/ # Favicony
|
||||
│
|
||||
├── resources/ # Zasoby binarne (syntaksy, motyw)
|
||||
├── contrib/cli/client # Skrypt bash CLI
|
||||
├── Cargo.toml # Manifest projektu
|
||||
├── build.rs # Wstrzykiwanie git hash do binarki
|
||||
├── Dockerfile # Wielostopniowy build
|
||||
├── docker-compose.yml
|
||||
└── fly.toml # Konfiguracja Fly.io
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Endpointy API
|
||||
|
||||
| Metoda | Ścieżka | Opis |
|
||||
|--------|---------|------|
|
||||
| GET | `/` | Strona główna z formularzem |
|
||||
| GET | `/static/<file>` | Zasoby statyczne (CSS, JS, czcionki) |
|
||||
| POST | `/` | Upload pliku binarnego → zwraca ID |
|
||||
| POST | `/submit` | Formularz tekstowy → redirect do `/p/<id>.<ext>` |
|
||||
| GET | `/<id>` | Surowa treść pasty |
|
||||
| GET | `/<id>.<ext>` | Surowa treść z rozszerzeniem (rank=1) |
|
||||
| GET | `/p/<id>` | Pasta z podświetlaniem składni (HTML) |
|
||||
| GET | `/p/<id>.<ext>` | Pasta z wymuszonym językiem (rank=1) |
|
||||
|
||||
**Priorytet routingu:** trasy z rozszerzeniem (`rank=1`) są dopasowywane przed trasami generycznymi (`rank=2`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Modele danych
|
||||
|
||||
### PasteId
|
||||
```rust
|
||||
pub struct PasteId<'a>(Cow<'a, str>)
|
||||
```
|
||||
- Generuje losowe 6-znakowe ID alfanumeryczne (36^6 ≈ 2,1 mld kombinacji)
|
||||
- Implementuje `FromParam` dla automatycznej walidacji URL w Rocket
|
||||
|
||||
### PasteIdSyntax
|
||||
```rust
|
||||
pub struct PasteIdSyntax<'a> { syn_id: Cow<'a, str> }
|
||||
```
|
||||
- Parsuje URL typu `/p/abc123.cpp` na nazwę pliku i rozszerzenie
|
||||
- `get_fname()` → `"abc123"`, `get_ext()` → `"cpp"`
|
||||
|
||||
### ResponseWrapper<R>
|
||||
```rust
|
||||
enum ResponseWrapper<R> {
|
||||
MetaInterfaceResponse(R),
|
||||
PrettyPasteContentResponse(R, SystemTime),
|
||||
RawPasteContentResponse(R, SystemTime),
|
||||
Redirect(Box<Redirect>),
|
||||
NotFound(String),
|
||||
ServerError(String),
|
||||
}
|
||||
```
|
||||
Centralnie zarządza nagłówkami HTTP:
|
||||
- `Server: bin v.<VERSION> (<GIT_HASH>)`
|
||||
- `ETag`, `Last-Modified`, `Cache-Control`
|
||||
|
||||
---
|
||||
|
||||
## 6. Zależności (Cargo.toml)
|
||||
|
||||
| Crate | Wersja | Zastosowanie |
|
||||
|-------|--------|--------------|
|
||||
| `rand` | 0.8.4 | Generowanie ID |
|
||||
| `rocket` | 0.5.0-rc.1 | Framework web |
|
||||
| `tree_magic` | 0.2.3 | Detekcja MIME |
|
||||
| `syntect` | 4.6.0 | Podświetlanie składni |
|
||||
| `rust-embed` | 6.3.0 | Osadzanie zasobów |
|
||||
| `clap` | 3.0.9 | Parsowanie CLI |
|
||||
| `once_cell` | 1 | Lazy static |
|
||||
| `sha256` | 1 | ETag |
|
||||
| `time` | 0.3 | Formatowanie czasu |
|
||||
| `rocket_dyn_templates` | 0.1.0-rc.1 | Szablony Tera |
|
||||
|
||||
---
|
||||
|
||||
## 7. System budowania
|
||||
|
||||
### build.rs
|
||||
Przechwytuje hash commita git i wstrzykuje go jako stałą czasu kompilacji (`GIT_HASH`).
|
||||
|
||||
### .cargo/config.toml
|
||||
- Kompilacja statyczna: `-C target-feature=+crt-static`
|
||||
- Domyślny target: `x86_64-unknown-linux-gnu`
|
||||
- Cross-kompilacja ARM64: `aarch64-linux-gnu-gcc`
|
||||
|
||||
### Docker (wielostopniowy)
|
||||
1. **Builder:** obraz Rust → `cargo build --release`
|
||||
2. **Runner:** obraz `scratch` (pusty) + tylko binarka
|
||||
3. Rezultat: minimalistyczny obraz (~20-30 MB)
|
||||
|
||||
---
|
||||
|
||||
## 8. Konfiguracja serwera
|
||||
|
||||
### Argumenty CLI
|
||||
|
||||
| Flag | Domyślnie | Opis |
|
||||
|------|-----------|------|
|
||||
| `-a` | `127.0.0.1` | Adres nasłuchu |
|
||||
| `-p` | `6162` | Port |
|
||||
| `-u` | `./upload` | Katalog przechowywania plików |
|
||||
| `-b` | `100 MiB` | Limit rozmiaru uploadu binarnego |
|
||||
| `-c` | off | Pokaż opis klienta CLI na stronie głównej |
|
||||
|
||||
### Zmienne środowiskowe (prefiks `BIN_`)
|
||||
```bash
|
||||
BIN_PORT=6163
|
||||
BIN_ADDRESS=0.0.0.0
|
||||
BIN_LIMITS={form="16 MiB"}
|
||||
BIN_WORKERS=8
|
||||
BIN_IDENT=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Architektura i wzorce projektowe
|
||||
|
||||
1. **Embedded Resources** — wszystkie zasoby (CSS, JS, czcionki, definicje syntaksy) skompilowane w binarce (`rust-embed`). Zero zależności zewnętrznych.
|
||||
|
||||
2. **Response Wrapper** — generyczny wrapper abstrakcji odpowiedzi z centralizowaną logiką nagłówków HTTP.
|
||||
|
||||
3. **Type-Safe URL Params** — `PasteId` i `PasteIdSyntax` implementują trait `FromParam` — Rocket automatycznie waliduje parametry URL.
|
||||
|
||||
4. **Lazy Static** — `BINARY_ETAG` obliczany raz przy starcie (SHA256 wersji) via `once_cell::sync::Lazy`.
|
||||
|
||||
5. **Build-Time Version Injection** — hash git osadzony w czasie kompilacji, widoczny w nagłówku `Server`.
|
||||
|
||||
6. **Flat Filesystem Storage** — brak bazy danych; pasty jako pliki w katalogu `./upload`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Frontend (JavaScript)
|
||||
|
||||
### index.js (~150 linii)
|
||||
- Drag-and-drop upload plików
|
||||
- Wklejanie obrazów ze schowka (`paste` event)
|
||||
- Tab jako 4 spacje (nie nawigacja formularza)
|
||||
- Ctrl+Enter → submit
|
||||
- Dynamiczne pokazywanie/ukrywanie UI
|
||||
- Fork przez localStorage
|
||||
|
||||
### pretty.js
|
||||
- Przełącznik zawijania: 3 stany (brak → auto → 80 znaków)
|
||||
- Fork: kopiuje treść do localStorage, przekierowuje na główną
|
||||
- Raw: przełącza `/p/<id>` → `/<id>`
|
||||
- New: przejście do strony głównej
|
||||
|
||||
---
|
||||
|
||||
## 11. Strategia cachowania
|
||||
|
||||
| Typ odpowiedzi | Cache-Control |
|
||||
|----------------|---------------|
|
||||
| Pasta pretty | `max-age=604800, stale-while-revalidate=86400` |
|
||||
| Pasta raw | `max-age=604800, immutable` |
|
||||
| Meta/Statyczne | ETag (SHA256 wersji) → `304 Not Modified` |
|
||||
|
||||
---
|
||||
|
||||
## 12. Statystyki kodu
|
||||
|
||||
| Metryka | Wartość |
|
||||
|---------|---------|
|
||||
| Pliki źródłowe Rust | 13 |
|
||||
| Linie kodu Rust | ~600 |
|
||||
| Szablony HTML | 3 |
|
||||
| Pliki JavaScript | 2 |
|
||||
| Arkusze CSS | 2 |
|
||||
| Obsługiwane języki (syntaksy) | 100+ |
|
||||
| Endpointy API | 7 |
|
||||
| Typy odpowiedzi HTTP | 6 |
|
||||
|
||||
---
|
||||
|
||||
## 13. Algorytmy kluczowe
|
||||
|
||||
### Generowanie ID
|
||||
```
|
||||
1. rand::thread_rng() → thread-safe RNG
|
||||
2. Alphanumeric distribution (a-z, A-Z, 0-9)
|
||||
3. Pobierz 6 próbek → String
|
||||
4. Opakuj w Cow::Owned
|
||||
```
|
||||
|
||||
### Detekcja MIME (upload binarny)
|
||||
```
|
||||
1. Wczytaj bajty pliku
|
||||
2. tree_magic::from_filepath() → MIME
|
||||
3. MIME zawiera "text" → /p/<id> (kolorowanie)
|
||||
4. Inaczej → /<id> (surowe pobranie)
|
||||
```
|
||||
|
||||
### Renderowanie składni
|
||||
```
|
||||
1. Parsuj URL /p/abc123.cpp
|
||||
2. Wyodrębnij rozszerzenie ("cpp")
|
||||
3. Znajdź definicję syntaxu w syntect
|
||||
4. Fallback → plain_text
|
||||
5. Renderuj HTML z motywem Ayu Dark
|
||||
```
|
||||
90
Cargo.lock
generated
90
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
@@ -14,7 +14,7 @@ version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -102,6 +102,7 @@ version = "2.2.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"once_cell",
|
||||
"pulldown-cmark",
|
||||
"rand",
|
||||
"rocket",
|
||||
"rocket_dyn_templates",
|
||||
@@ -133,6 +134,12 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.7.3"
|
||||
@@ -169,7 +176,7 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -255,7 +262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"clap_derive",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
@@ -284,7 +291,7 @@ version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -364,7 +371,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841ef46f4787d9097405cac4e70fb8644fc037b526e8c14054247c0263c400d0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
@@ -481,7 +488,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"fsevent-sys",
|
||||
]
|
||||
|
||||
@@ -500,7 +507,7 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"fuchsia-zircon-sys",
|
||||
]
|
||||
|
||||
@@ -593,7 +600,7 @@ dependencies = [
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
@@ -631,6 +638,15 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.4"
|
||||
@@ -667,7 +683,7 @@ version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -792,7 +808,7 @@ dependencies = [
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
"regex",
|
||||
"same-file",
|
||||
"thread_local",
|
||||
@@ -823,7 +839,7 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
@@ -978,9 +994,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.1"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
@@ -1075,7 +1091,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
"mime",
|
||||
"spin",
|
||||
"tokio",
|
||||
@@ -1118,7 +1134,7 @@ version = "4.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"filetime",
|
||||
"fsevent",
|
||||
"fsevent-sys",
|
||||
@@ -1189,7 +1205,7 @@ version = "6.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67ddfe2c93bb389eea6e6d713306880c7f6dcc99a75b659ce145d962c861b225"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"onig_sys",
|
||||
@@ -1223,7 +1239,7 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
||||
dependencies = [
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1495,6 +1511,18 @@ dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"getopts",
|
||||
"memchr 2.8.0",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.15"
|
||||
@@ -1556,7 +1584,7 @@ version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1586,7 +1614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
@@ -1631,7 +1659,7 @@ dependencies = [
|
||||
"futures",
|
||||
"indexmap",
|
||||
"log",
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
"multer",
|
||||
"num_cpus",
|
||||
"parking_lot 0.11.2",
|
||||
@@ -1695,7 +1723,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"indexmap",
|
||||
"log",
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
"mime",
|
||||
"parking_lot 0.11.2",
|
||||
"pear",
|
||||
@@ -1956,7 +1984,7 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045"
|
||||
dependencies = [
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2050,7 +2078,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"flate2",
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
@@ -2182,7 +2210,7 @@ checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"memchr 2.4.1",
|
||||
"memchr 2.8.0",
|
||||
"mio 0.7.14",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
@@ -2405,6 +2433,18 @@ dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
|
||||
@@ -15,6 +15,7 @@ clap = { version = "3.0.9", features = ["derive", "env"] }
|
||||
once_cell = "1"
|
||||
sha256 = "1"
|
||||
time = { version = "0.3", features = ["formatting"] }
|
||||
pulldown-cmark = "0.9"
|
||||
|
||||
[dependencies.rocket_dyn_templates]
|
||||
version = "0.1.0-rc.1"
|
||||
|
||||
@@ -35,6 +35,7 @@ fn setup_tera_engine(tera: &mut Tera) {
|
||||
let base_html = EmbeddedTemplates::get("base.html.tera").unwrap();
|
||||
let index_html = EmbeddedTemplates::get("index.html.tera").unwrap();
|
||||
let pretty_html = EmbeddedTemplates::get("pretty.html.tera").unwrap();
|
||||
let markdown_html = EmbeddedTemplates::get("markdown.html.tera").unwrap();
|
||||
|
||||
// and shove them in the tera instance
|
||||
tera.add_raw_templates(vec![
|
||||
@@ -44,6 +45,10 @@ fn setup_tera_engine(tera: &mut Tera) {
|
||||
"pretty.html",
|
||||
std::str::from_utf8(&pretty_html.data).unwrap(),
|
||||
),
|
||||
(
|
||||
"markdown.html",
|
||||
std::str::from_utf8(&markdown_html.data).unwrap(),
|
||||
),
|
||||
])
|
||||
.expect("Could not add raw templates to the tera instance");
|
||||
}
|
||||
@@ -108,7 +113,8 @@ fn rocket() -> _ {
|
||||
routes::retrieve::retrieve,
|
||||
routes::retrieve::retrieve_ext,
|
||||
routes::pretty_retrieve::pretty_retrieve,
|
||||
routes::pretty_retrieve::pretty_retrieve_ext
|
||||
routes::pretty_retrieve::pretty_retrieve_ext,
|
||||
routes::markdown_retrieve::markdown_retrieve
|
||||
],
|
||||
)
|
||||
.attach(shield)
|
||||
|
||||
110
src/models/markdown.rs
Normal file
110
src/models/markdown.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use syntect::highlighting::{Theme, ThemeSet};
|
||||
use syntect::html::highlighted_html_for_string;
|
||||
use syntect::parsing::SyntaxSet;
|
||||
|
||||
static SYNTAXES: &[u8] =
|
||||
include_bytes!("../../resources/syntaxes/syntaxes.bin");
|
||||
static AYU_DARK: &[u8] =
|
||||
include_bytes!("../../resources/themes/ayu_dark.tmTheme");
|
||||
|
||||
pub const THEMES: &[(&str, &str)] = &[
|
||||
("ayu-dark", "Ayu Dark"),
|
||||
("base16-ocean-dark", "Base16 Ocean Dark"),
|
||||
("base16-eighties", "Base16 Eighties"),
|
||||
("base16-mocha", "Base16 Mocha"),
|
||||
("base16-ocean-light", "Base16 Ocean Light"),
|
||||
("github", "GitHub"),
|
||||
("solarized-dark", "Solarized Dark"),
|
||||
("solarized-light", "Solarized Light"),
|
||||
];
|
||||
|
||||
fn load_theme(name: &str) -> Theme {
|
||||
match name {
|
||||
"ayu-dark" | "" => {
|
||||
let mut cursor = std::io::Cursor::new(AYU_DARK);
|
||||
ThemeSet::load_from_reader(&mut cursor).unwrap()
|
||||
}
|
||||
other => {
|
||||
let key = match other {
|
||||
"github" => "InspiredGitHub",
|
||||
"solarized-dark" => "Solarized (dark)",
|
||||
"solarized-light" => "Solarized (light)",
|
||||
"base16-ocean-dark" => "base16-ocean.dark",
|
||||
"base16-eighties" => "base16-eighties.dark",
|
||||
"base16-mocha" => "base16-mocha.dark",
|
||||
"base16-ocean-light" => "base16-ocean.light",
|
||||
_ => "base16-ocean.dark",
|
||||
};
|
||||
let mut ts = ThemeSet::load_defaults();
|
||||
ts.themes.remove(key).unwrap_or_else(|| {
|
||||
let mut cursor = std::io::Cursor::new(AYU_DARK);
|
||||
ThemeSet::load_from_reader(&mut cursor).unwrap()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_markdown_body(path: &Path, theme_name: &str) -> std::io::Result<String> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
|
||||
let ss: SyntaxSet = syntect::dumps::from_binary(SYNTAXES);
|
||||
let theme = load_theme(theme_name);
|
||||
|
||||
let options = Options::ENABLE_TABLES
|
||||
| Options::ENABLE_FOOTNOTES
|
||||
| Options::ENABLE_STRIKETHROUGH
|
||||
| Options::ENABLE_TASKLISTS;
|
||||
|
||||
let parser = Parser::new_ext(&content, options);
|
||||
|
||||
let mut html_output = String::new();
|
||||
let mut in_code_block = false;
|
||||
let mut code_lang = String::new();
|
||||
let mut code_content = String::new();
|
||||
|
||||
for event in parser {
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref lang))) => {
|
||||
in_code_block = true;
|
||||
code_lang = lang.to_string();
|
||||
code_content.clear();
|
||||
}
|
||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) => {
|
||||
in_code_block = true;
|
||||
code_lang.clear();
|
||||
code_content.clear();
|
||||
}
|
||||
Event::End(Tag::CodeBlock(_)) => {
|
||||
in_code_block = false;
|
||||
let syntax = if code_lang.is_empty() {
|
||||
ss.find_syntax_plain_text()
|
||||
} else {
|
||||
ss.find_syntax_by_token(&code_lang)
|
||||
.unwrap_or_else(|| ss.find_syntax_plain_text())
|
||||
};
|
||||
let highlighted = highlighted_html_for_string(
|
||||
&code_content,
|
||||
&ss,
|
||||
syntax,
|
||||
&theme,
|
||||
);
|
||||
html_output.push_str(&highlighted);
|
||||
code_content.clear();
|
||||
}
|
||||
Event::Text(ref text) if in_code_block => {
|
||||
code_content.push_str(text);
|
||||
}
|
||||
other => {
|
||||
pulldown_cmark::html::push_html(
|
||||
&mut html_output,
|
||||
std::iter::once(other),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(html_output)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod markdown;
|
||||
pub mod paste_id;
|
||||
pub mod pretty;
|
||||
pub mod pretty_syntax;
|
||||
|
||||
63
src/routes/markdown_retrieve.rs
Normal file
63
src/routes/markdown_retrieve.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use rocket_dyn_templates::Template;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::ErrorKind::NotFound;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::get_upload_dir;
|
||||
use crate::models::markdown::{get_markdown_body, THEMES};
|
||||
use crate::models::paste_id::PasteId;
|
||||
use crate::models::response_wrapper::ResponseWrapper;
|
||||
|
||||
#[get("/m/<id>?<theme>")]
|
||||
pub async fn markdown_retrieve(
|
||||
id: PasteId<'_>,
|
||||
theme: Option<&str>,
|
||||
) -> ResponseWrapper<Template> {
|
||||
let id = id.to_string();
|
||||
let theme = theme.unwrap_or("ayu-dark");
|
||||
let filepath = Path::new(&get_upload_dir()).join(&id);
|
||||
|
||||
let modified_date =
|
||||
match fs::metadata(&filepath).and_then(|m| m.modified()) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
return ResponseWrapper::not_found(&id);
|
||||
}
|
||||
Err(e) => {
|
||||
return ResponseWrapper::server_error(e.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let contents = match get_markdown_body(&filepath, theme) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
return ResponseWrapper::not_found(&id);
|
||||
}
|
||||
Err(e) => {
|
||||
return ResponseWrapper::server_error(e.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
// Build theme options HTML for the selector
|
||||
let theme_options: String = THEMES
|
||||
.iter()
|
||||
.map(|(key, label)| {
|
||||
if *key == theme {
|
||||
format!(r#"<option value="{}" selected>{}</option>"#, key, label)
|
||||
} else {
|
||||
format!(r#"<option value="{}">{}</option>"#, key, label)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("title", id.clone());
|
||||
map.insert("body", contents);
|
||||
map.insert("paste_id", id);
|
||||
map.insert("current_theme", theme.to_string());
|
||||
map.insert("theme_options", theme_options);
|
||||
let rendered = Template::render("markdown.html", &map);
|
||||
|
||||
ResponseWrapper::pretty_paste_response(rendered, modified_date)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod index;
|
||||
pub mod markdown_retrieve;
|
||||
pub mod pretty_retrieve;
|
||||
pub mod retrieve;
|
||||
pub mod static_files;
|
||||
|
||||
206
static/css/markdown.css
Normal file
206
static/css/markdown.css
Normal file
@@ -0,0 +1,206 @@
|
||||
/* Ayu Dark markdown styles */
|
||||
|
||||
#markdownContent {
|
||||
max-width: 104ch;
|
||||
margin: 0 auto;
|
||||
padding: 20px 10px 80px 10px;
|
||||
font-family: 'Iosevka Web', monospace;
|
||||
line-height: 1.6;
|
||||
color: #E6E1CF;
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
#markdownContent h1,
|
||||
#markdownContent h2,
|
||||
#markdownContent h3,
|
||||
#markdownContent h4,
|
||||
#markdownContent h5,
|
||||
#markdownContent h6 {
|
||||
color: #ffb454;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#markdownContent h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #253340;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
#markdownContent h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #253340;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
#markdownContent h3 { font-size: 1.25em; }
|
||||
#markdownContent h4 { font-size: 1.1em; }
|
||||
#markdownContent h5 { font-size: 1em; }
|
||||
|
||||
#markdownContent h6 {
|
||||
font-size: 0.9em;
|
||||
color: #b8b4a0;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
#markdownContent a {
|
||||
color: #59c2ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#markdownContent a:hover {
|
||||
text-decoration: underline;
|
||||
color: #7fd0ff;
|
||||
}
|
||||
|
||||
/* Paragraphs and lists */
|
||||
#markdownContent p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#markdownContent ul,
|
||||
#markdownContent ol {
|
||||
margin: 1em 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
#markdownContent li {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
#markdownContent li > p {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
/* Blockquote */
|
||||
#markdownContent blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 0.6em 1em;
|
||||
border-left: 4px solid #f29718;
|
||||
background: #131b24;
|
||||
color: #b8b4a0;
|
||||
}
|
||||
|
||||
#markdownContent blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
#markdownContent code {
|
||||
background: #131b24;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-family: 'Iosevka Web', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #f07178;
|
||||
}
|
||||
|
||||
/* Syntect code blocks — wrap in markdown-code-block div */
|
||||
#markdownContent pre {
|
||||
margin: 1em 0;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Iosevka Web', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#markdownContent pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
#markdownContent table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#markdownContent th,
|
||||
#markdownContent td {
|
||||
border: 1px solid #253340;
|
||||
padding: 0.5em 1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#markdownContent th {
|
||||
background: #131b24;
|
||||
color: #ffb454;
|
||||
}
|
||||
|
||||
#markdownContent tr:nth-child(even) {
|
||||
background: #131b24;
|
||||
}
|
||||
|
||||
/* Horizontal rule */
|
||||
#markdownContent hr {
|
||||
border: none;
|
||||
border-top: 1px solid #253340;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
#markdownContent img {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Task list checkboxes */
|
||||
#markdownContent input[type="checkbox"] {
|
||||
margin-right: 0.4em;
|
||||
accent-color: #f29718;
|
||||
}
|
||||
|
||||
/* Strikethrough */
|
||||
#markdownContent del {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
/* Footnotes */
|
||||
#markdownContent .footnote-definition {
|
||||
font-size: 0.85em;
|
||||
color: #b8b4a0;
|
||||
border-top: 1px solid #253340;
|
||||
margin-top: 2em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
.topRightBox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rootBox {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#themePicker {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
#themeSelect {
|
||||
background: #0f1419;
|
||||
color: #f29718;
|
||||
border: 1px solid #253340;
|
||||
font-family: 'Iosevka Web', monospace;
|
||||
font-size: 14px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#themeSelect:hover {
|
||||
border-color: #f29718;
|
||||
}
|
||||
|
||||
#themeSelect option {
|
||||
background: #0f1419;
|
||||
color: #E6E1CF;
|
||||
}
|
||||
22
static/js/markdown.js
Normal file
22
static/js/markdown.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const homePage = document.location.origin;
|
||||
|
||||
function rawClicked() {
|
||||
window.location = homePage + '/' + PASTE_ID;
|
||||
}
|
||||
|
||||
async function forkClicked() {
|
||||
const response = await fetch(homePage + '/' + PASTE_ID);
|
||||
const text = await response.text();
|
||||
localStorage["forkText"] = text;
|
||||
window.location = homePage;
|
||||
}
|
||||
|
||||
function newPasteClicked() {
|
||||
window.location = homePage;
|
||||
}
|
||||
|
||||
function themeChanged(theme) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('theme', theme);
|
||||
window.location = url.toString();
|
||||
}
|
||||
28
templates/markdown.html.tera
Normal file
28
templates/markdown.html.tera
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="/static/css/markdown.css">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block body %}
|
||||
<div class="rootBox">
|
||||
<div id="markdownContent">
|
||||
{{ body | safe }}
|
||||
</div>
|
||||
<div class="topRightBox">
|
||||
<button id="rawBtn" onclick="rawClicked()">= Raw</button>
|
||||
<button id="forkBtn" onclick="forkClicked()">⑂ Fork</button>
|
||||
<button id="newPasteBtn" onclick="newPasteClicked()">+ New</button>
|
||||
</div>
|
||||
<div id="themePicker">
|
||||
<select id="themeSelect" onchange="themeChanged(this.value)">
|
||||
{{ theme_options | safe }}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PASTE_ID = "{{ paste_id }}";
|
||||
</script>
|
||||
<script src="/static/js/markdown.js"></script>
|
||||
{% endblock body %}
|
||||
Reference in New Issue
Block a user