wikimark 0.5.2

Markdown-based wiki stored in a git repo
Documentation
use super::{errors, md2html, page, WikiState};
use axum::{
    extract::{Path, State, Query, Form},
    response::{Html, IntoResponse, Response, Redirect},
};
use axum_extra::TypedHeader;
use axum_extra::headers::{Header, HeaderName, HeaderValue, Error as HeaderError};
use serde_derive::Deserialize;
use serde_yaml::Value;
use minijinja::context;
use std::sync::Arc;
use std::collections::BTreeMap;

type Result<T> = std::result::Result<T, errors::AppError>;

#[derive(Debug)]
pub struct User(String);

static USER_HEADER_NAME: HeaderName = HeaderName::from_static("x-forwarded-user");
impl Header for User {
    fn name() -> &'static HeaderName {
        &USER_HEADER_NAME
    }
    fn decode<'i, I>(values: &mut I) -> std::result::Result<Self, HeaderError>
    where
        Self: Sized,
        I: Iterator<Item = &'i HeaderValue>,
    {
        let user = std::env::var("WIKIMARK_USER").ok();
        if let Some(u) = user {
            return Ok(User(u.to_string()));
        }
        let value = values.next().ok_or_else(HeaderError::invalid)?;
        Ok(User(
            value
                .to_str()
                .map_err(|_| HeaderError::invalid())?
                .to_owned(),
        ))
    }
    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
        let v = HeaderValue::from_str(&self.0).expect("Username is invalid as header value");
        values.extend(std::iter::once(v));
    }
}

/// A CSS response.
///
/// Will automatically get `Content-Type: text/css`.
#[derive(Clone, Debug)]
pub struct Css<T>(pub T);

impl<T> IntoResponse for Css<T>
where
    T: Into<String>,
{
    fn into_response(self) -> Response {
        (
            [(
                http::header::CONTENT_TYPE,
                HeaderValue::from_static("text/css"),
            )],
            self.0.into(),
        )
            .into_response()
    }
}

impl<T> From<T> for Css<T> {
    fn from(inner: T) -> Self {
        Self(inner)
    }
}

type UserHeader = TypedHeader<User>;

pub async fn index() -> impl IntoResponse {
    Redirect::permanent("./page/")
}

pub async fn page(
    State(state): State<Arc<WikiState>>,
    user: Option<UserHeader>,
    fname: Option<Path<String>>,
) -> Result<Html<String>> {
    let repo = state.repo.local();
    let fname = fname.unwrap_or_else(|| Path("".to_owned())).0;
    let (md, directory) = page::get_page(&repo, &fname)?;
    let templ_file = if directory { "dir.html" } else { "page.html" };
    let entries = if directory {
        Some(page::list_files(&repo, &fname, false)?)
    } else {
        None
    };
    let templ = state.env.get_template(templ_file).unwrap();
    let user_str = user.as_ref().map(|u| u.0 .0.as_str());
    let page = md2html::parse(&md.content, &md.meta);
    Ok(Html(templ.render(context!(
        user => user_str,
        toc => page.toc,
        meta => md.meta,
        content => page.content,
        link => fname,
        children => entries,
    ))?))
}

pub async fn pages(
    State(state): State<Arc<WikiState>>,
    user: Option<UserHeader>,
) -> Result<Html<String>> {
    let templ = state.env.get_template("pages.html").unwrap();
    let user_str = user.as_ref().map(|u| u.0 .0.as_str());
    let pages = page::list_files(&state.repo.local(), "", true)?;
    Ok(Html(templ.render(context!(
        user => user_str,
        pages,
    ))?))
}

pub async fn changelog(
    State(state): State<Arc<WikiState>>,
    user: Option<UserHeader>,
) -> Result<Html<String>> {
    let templ = state.env.get_template("changelog.html").unwrap();
    let user_str = user.as_ref().map(|u| u.0 .0.as_str());
    Ok(Html(templ.render(context!(
        user => user_str,
        log => state.repo.local().get_log()?,
        commit_url_prefix => state.commit_url_prefix,
    ))?))
}

#[derive(Deserialize)]
pub struct EditQuery {
    page: Option<String>,
}

pub async fn edit(State(state): State<Arc<WikiState>>, user: UserHeader, Query(q): Query<EditQuery>) -> Result<Html<String>> {
    let repo = state.repo.local();
    let user_str = user.0 .0.as_str();
    let templ = state.env.get_template("edit.html").unwrap();
    if let Some(page) = q.page {
        let (md, directory) = page::get_page(&repo, &page)?;
        let mut path = std::path::PathBuf::from(page);
        path.pop();
        Ok(Html(templ.render(context!(
            user => user_str,
            page => md,
            path => path,
            directory => directory,
        ))?))
    } else {
        Ok(Html(templ.render(context!(
            user => user_str,
        ))?))
    }
}

#[derive(Deserialize, Debug)]
pub struct CommitForm {
    parent: String,
    content: String,
    title: String,
    #[serde(default)]
    private: bool,
    #[serde(default)]
    directory: bool,
    #[serde(flatten)]
    other: BTreeMap<String, Value>,
}
pub async fn commit(
    State(state): State<Arc<WikiState>>,
    user: UserHeader,
    Form(form): Form<CommitForm>,
) -> Result<impl IntoResponse> {
    let info = page::PageUpdate {
        parent: form.parent,
        directory: form.directory,
        page: page::RawPage {
            content: form.content,
            meta: page::Metadata {
                title: form.title,
                private: form.private,
                other: form.other,
            },
        }
    };
    let ret = page::commit_page(&state.repo.local(), user.0 .0, info)?;
    Ok(Redirect::to(&format!("./page/{ret}")))
}

pub async fn css() -> Css<String> {
    Css(super::CSS.to_owned())
}

pub async fn assets(Path(path): Path<String>) -> Result<Response> {
    if let Some(f) = super::STATIC_ASSETS.get_file(&path) {
        let mime = mime_guess::from_path(&path).first_or_octet_stream();
        Ok((
            [(
                http::header::CONTENT_TYPE,
                HeaderValue::from_str(mime.essence_str())?,
            )],
            f.contents(),
        )
            .into_response())
    } else {
        Ok(http::StatusCode::NOT_FOUND.into_response())
    }
}