feat: markdown rendering /m/<id>
Some checks failed
Build CI / pre-build-checks (push) Failing after 16s
Build CI / build (amd64) (push) Has been skipped
Build CI / build (arm64) (push) Has been skipped

This commit is contained in:
2026-04-21 16:24:08 +02:00
parent a03bb47b80
commit c9132e836a
11 changed files with 772 additions and 26 deletions

268
.llm/analysis.md Normal file
View 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
View File

@@ -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"

View File

@@ -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"

View File

@@ -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
View 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)
}

View File

@@ -1,3 +1,4 @@
pub mod markdown;
pub mod paste_id;
pub mod pretty;
pub mod pretty_syntax;

View 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)
}

View File

@@ -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
View 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
View 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();
}

View 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()">&#x2442; Fork</button>
<button id="newPasteBtn" onclick="newPasteClicked()">&#43; 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 %}