karnaugh/src/main.rs

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(),
}
}