150 lines
4.4 KiB
Rust
150 lines
4.4 KiB
Rust
use std::{
|
|
net::{Ipv4Addr, SocketAddrV4},
|
|
path::PathBuf,
|
|
sync::Arc,
|
|
};
|
|
|
|
use ::typst::syntax::VirtualPath;
|
|
use axum::{
|
|
Router,
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::{Html, IntoResponse, Response},
|
|
routing::get,
|
|
};
|
|
use clap::Parser;
|
|
use itertools::Itertools;
|
|
use tower_http::{
|
|
compression::CompressionLayer,
|
|
services::{ServeDir, ServeFile},
|
|
trace::TraceLayer,
|
|
};
|
|
use tracing::{Level, info};
|
|
use tracing_subscriber::FmtSubscriber;
|
|
|
|
use crate::typst::TypstContext;
|
|
|
|
mod typst;
|
|
|
|
#[derive(Clone)]
|
|
struct AppState {
|
|
typst: Arc<TypstContext>,
|
|
config: AppConfig,
|
|
}
|
|
|
|
/// A Typst-based (static) site generator.
|
|
#[derive(Clone, Parser)]
|
|
#[command(version, about)]
|
|
struct AppConfig {
|
|
/// The port to expose the site on.
|
|
#[arg(short = 'p', long, default_value_t = 3000)]
|
|
port: u16,
|
|
|
|
/// The directory where typst files and related content are located.
|
|
#[arg(short = 'c', long, value_name = "DIR", default_value = "./content")]
|
|
content_root: PathBuf,
|
|
|
|
/// The directory relative to content root where local packages are installed.
|
|
#[arg(short = 's', long, value_name = "DIR", default_value = "common")]
|
|
common_root: PathBuf,
|
|
|
|
/// The directory relative to content root where assets are stored.
|
|
/// Should be inside the content root to ensure that typst ran externally from
|
|
/// karnaugh can also find them, this is not required however.
|
|
#[arg(short = 'a', long, value_name = "DIR", default_value = "assets")]
|
|
assets_root: PathBuf,
|
|
|
|
/// The directory where packages required to render pages will be downloaded.
|
|
#[arg(short = 'd', long, value_name = "DIR", default_value = "/tmp/karnaugh")]
|
|
packages_root: PathBuf,
|
|
|
|
/// The log level to use
|
|
#[arg(short = 'l', long, value_name = "LEVEL", default_value_t = Level::INFO)]
|
|
log_level: Level,
|
|
}
|
|
|
|
impl AppConfig {
|
|
fn get_full_common_path(&self) -> PathBuf {
|
|
let mut root = self.content_root.clone();
|
|
root.push(&self.common_root);
|
|
root
|
|
}
|
|
|
|
fn get_full_assets_path(&self) -> PathBuf {
|
|
let mut root = self.content_root.clone();
|
|
root.push(&self.assets_root);
|
|
root
|
|
}
|
|
}
|
|
|
|
impl AppState {
|
|
fn new() -> Self {
|
|
let config = AppConfig::parse();
|
|
let typst = Arc::new(TypstContext::new(config.clone()));
|
|
Self { typst, config }
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
let state = AppState::new();
|
|
|
|
FmtSubscriber::builder()
|
|
.with_max_level(state.config.log_level)
|
|
.init();
|
|
|
|
// TODO: Replace with config option
|
|
let mut favicon = state.config.get_full_assets_path();
|
|
favicon.push(PathBuf::from("favicon.svg"));
|
|
|
|
let app = Router::new()
|
|
.route("/", get(root_handler))
|
|
.route("/{*path}", get(typst_handler))
|
|
.route_service("/favicon.ico", ServeFile::new(favicon))
|
|
.nest_service("/assets", ServeDir::new(&state.config.assets_root))
|
|
.with_state(state.clone())
|
|
.layer(TraceLayer::new_for_http())
|
|
.layer(CompressionLayer::new());
|
|
|
|
let socket = SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), state.config.port);
|
|
info!("Serving Karnaugh on socket {}", socket);
|
|
let listener = tokio::net::TcpListener::bind(socket)
|
|
.await
|
|
.expect(&format!("Failed to bind to socket {}", socket));
|
|
axum::serve(listener, app)
|
|
.await
|
|
.expect("Failed to serve site");
|
|
}
|
|
|
|
async fn root_handler(State(state): State<AppState>) -> impl IntoResponse {
|
|
typst_handler(State(state), Path("index".into())).await
|
|
}
|
|
|
|
async fn typst_handler(State(state): State<AppState>, Path(path): Path<String>) -> Response {
|
|
if path.starts_with('/') || path.contains("..") || path.contains('\\') {
|
|
return StatusCode::BAD_REQUEST.into_response();
|
|
}
|
|
|
|
let path = match VirtualPath::new(path) {
|
|
Ok(path) => path,
|
|
Err(_) => return StatusCode::BAD_REQUEST.into_response(),
|
|
};
|
|
let path = path.with_extension("typ");
|
|
|
|
match state.typst.compile_document(path) {
|
|
Ok(content) => Html(content).into_response(),
|
|
Err(err) => (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
// TODO: Fancy error formatting
|
|
Html(
|
|
#[allow(unstable_name_collisions)]
|
|
err.into_iter()
|
|
.map(|err| format!("{:?}", err))
|
|
.intersperse(String::from("<br/>"))
|
|
.collect::<String>(),
|
|
),
|
|
)
|
|
.into_response(),
|
|
}
|
|
}
|