Files
rustpad/src/assets.rs
T

167 lines
4.7 KiB
Rust

/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use axum::{
http::{HeaderValue, header},
response::{Html, IntoResponse, Response},
};
const MODULES: &[&str] = &[
"api",
"authorship",
"auth-ui",
"clipboard",
"collaboration",
"collaboration-session",
"editor-format",
"emoji-data",
"emoji-picker",
"image-alias",
"image-upload",
"logger",
"line-links",
"markdown",
"modal",
"note-api",
"note-editor",
"note-files",
"preview-edit",
"render-queue",
"session",
"socket",
"toast",
"theme",
"url-state",
"vendor-libs",
"security",
];
pub fn render_html(
template: &str,
asset_version: &str,
registration_enabled: bool,
external_auth: bool,
frontend_log_level: &str,
upload_max_size_bytes: usize,
entrypoint: &str,
) -> Response {
let urls = AssetUrls::new(asset_version);
let frontend_config = frontend_config(
frontend_log_level,
upload_max_size_bytes,
external_auth,
asset_version,
);
let app_stylesheets = format!(
"{}{}",
urls.stylesheet("styles"),
urls.stylesheet_path("libs/rustpad-player/player.css"),
);
let html = template
.replace("__APP_THEME_BOOTSTRAP__", theme_bootstrap())
.replace("__APP_STYLESHEET__", &app_stylesheets)
.replace("__APP_IMPORT_MAP__", &urls.import_map())
.replace("__APP_ENTRYPOINT__", &urls.entrypoint(entrypoint))
.replace(
"__REGISTRATION_ENABLED__",
if registration_enabled {
"true"
} else {
"false"
},
)
.replace(
"</head>",
&format!(
r#"<link rel="icon" href="/favicon.svg" type="image/svg+xml"><link rel="icon" href="/favicon.ico" sizes="any"><link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32"><link rel="apple-touch-icon" href="/apple-touch-icon.png">{frontend_config}</head>"#
),
);
let mut response = Html(html).into_response();
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("private, no-cache, no-store"),
);
response
}
pub fn theme_bootstrap() -> &'static str {
r#"<script>(()=>{const key="rustpad:theme";let theme=matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";try{const saved=localStorage.getItem(key);if(saved==="light"||saved==="dark")theme=saved}catch{}const root=document.documentElement;root.dataset.theme=theme;root.style.colorScheme=theme;const meta=document.querySelector('meta[name="color-scheme"]');if(meta)meta.content=theme})();</script>"#
}
pub fn stylesheet_tag(asset_version: &str, name: &str) -> String {
AssetUrls::new(asset_version).stylesheet(name)
}
fn frontend_config(
frontend_log_level: &str,
upload_max_size_bytes: usize,
external_auth: bool,
asset_version: &str,
) -> String {
format!(
r#"<script>window.__RUSTPAD_CONFIG__=Object.freeze({{frontendLogLevel:"{}",uploadMaxSizeBytes:{},externalAuth:{},assetVersion:"{}"}});</script>"#,
escape_js_string(frontend_log_level),
upload_max_size_bytes,
external_auth,
escape_js_string(asset_version),
)
}
struct AssetUrls<'a> {
version: &'a str,
}
impl<'a> AssetUrls<'a> {
fn new(version: &'a str) -> Self {
Self { version }
}
fn url(&self, path: &str) -> String {
format!("/assets/{path}?v={}", self.version)
}
fn stylesheet(&self, name: &str) -> String {
self.stylesheet_path(&format!("css/{name}.css"))
}
fn stylesheet_path(&self, path: &str) -> String {
format!(r#"<link rel="stylesheet" href="{}">"#, self.url(path))
}
fn entrypoint(&self, name: &str) -> String {
format!(
r#"<script type="module" src="{}"></script>"#,
self.url(&format!("js/{name}.js"))
)
}
fn import_map(&self) -> String {
let imports = MODULES
.iter()
.map(|module| {
format!(
r#""@rustpad/{module}":"{}""#,
self.url(&format!("js/{module}.js"))
)
})
.collect::<Vec<_>>()
.join(",");
format!(r#"<script type="importmap">{{"imports":{{{imports}}}}}</script>"#)
}
}
fn escape_js_string(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('<', "\\u003c")
}