From 5e6b84bed554deeda2e86b4539d62ab8da27fc4c Mon Sep 17 00:00:00 2001 From: liquidev Date: Wed, 4 Sep 2024 21:48:42 +0200 Subject: [PATCH] cache busting for faster load times, and seamless updates. because for some reason ServeDir can't do it correctly, and it tells the client "yeah hey nothing changed" even if something changed --- Cargo.lock | 33 ++++- crates/haku/src/compiler.rs | 1 - crates/rkgk/Cargo.toml | 4 +- .../src/{live_reload.rs => auto_reload.rs} | 0 crates/rkgk/src/build.rs | 129 ++++++++++++++++++ crates/rkgk/src/build/import_map.rs | 67 +++++++++ crates/rkgk/src/build/include_static.rs | 31 +++++ crates/rkgk/src/build/static_urls.rs | 79 +++++++++++ crates/rkgk/src/config.rs | 4 +- crates/rkgk/src/main.rs | 122 ++--------------- crates/rkgk/src/router.rs | 97 +++++++++++++ rkgk.toml | 10 +- static/404.html | 17 +++ static/base.css | 44 ------ static/canvas-renderer.js | 6 +- static/haku.js | 2 +- static/index.js | 8 +- static/online-users.js | 4 +- static/resize-handle.js | 2 +- static/session.js | 2 +- static/wall.js | 4 +- template/docs.hbs.html | 7 +- template/fonts.hbs.css | 44 ++++++ static/index.html => template/index.hbs.html | 66 +++++---- 24 files changed, 580 insertions(+), 203 deletions(-) rename crates/rkgk/src/{live_reload.rs => auto_reload.rs} (100%) create mode 100644 crates/rkgk/src/build.rs create mode 100644 crates/rkgk/src/build/import_map.rs create mode 100644 crates/rkgk/src/build/include_static.rs create mode 100644 crates/rkgk/src/build/static_urls.rs create mode 100644 crates/rkgk/src/router.rs create mode 100644 static/404.html create mode 100644 template/fonts.hbs.css rename static/index.html => template/index.hbs.html (69%) diff --git a/Cargo.lock b/Cargo.lock index c80b966..fe86550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,6 +213,19 @@ dependencies = [ "digest", ] +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -254,12 +267,13 @@ checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.1.8" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" +checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -309,6 +323,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "copy_dir" version = "0.1.3" @@ -780,6 +800,7 @@ checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown", + "serde", ] [[package]] @@ -1250,6 +1271,7 @@ dependencies = [ "argon2", "axum", "base64 0.22.1", + "blake3", "chrono", "color-eyre", "copy_dir", @@ -1260,6 +1282,7 @@ dependencies = [ "handlebars", "indexmap", "jotdown", + "mime_guess", "rand", "rand_chacha", "rayon", @@ -1427,6 +1450,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" diff --git a/crates/haku/src/compiler.rs b/crates/haku/src/compiler.rs index 7a87f2c..823aa50 100644 --- a/crates/haku/src/compiler.rs +++ b/crates/haku/src/compiler.rs @@ -4,7 +4,6 @@ use core::{ }; use alloc::vec::Vec; -use log::info; use crate::{ ast::{Ast, NodeId, NodeKind}, diff --git a/crates/rkgk/Cargo.toml b/crates/rkgk/Cargo.toml index e9b3192..5622fd6 100644 --- a/crates/rkgk/Cargo.toml +++ b/crates/rkgk/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" argon2 = "0.5.3" axum = { version = "0.7.5", features = ["macros", "ws"] } base64 = "0.22.1" +blake3 = "1.5.4" chrono = "0.4.38" color-eyre = "0.6.3" copy_dir = "0.1.3" @@ -15,8 +16,9 @@ derive_more = { version = "1.0.0", features = ["try_from"] } eyre = "0.6.12" haku.workspace = true handlebars = "6.0.0" -indexmap = "2.4.0" +indexmap = { version = "2.4.0", features = ["serde"] } jotdown = "0.5.0" +mime_guess = "2.0.5" rand = "0.8.5" rand_chacha = "0.3.1" rayon = "1.10.0" diff --git a/crates/rkgk/src/live_reload.rs b/crates/rkgk/src/auto_reload.rs similarity index 100% rename from crates/rkgk/src/live_reload.rs rename to crates/rkgk/src/auto_reload.rs diff --git a/crates/rkgk/src/build.rs b/crates/rkgk/src/build.rs new file mode 100644 index 0000000..7720945 --- /dev/null +++ b/crates/rkgk/src/build.rs @@ -0,0 +1,129 @@ +use std::{ + ffi::OsStr, + fs::{copy, create_dir_all, remove_dir_all, write}, +}; + +use copy_dir::copy_dir; +use eyre::Context; +use handlebars::Handlebars; +use import_map::ImportMap; +use include_static::IncludeStatic; +use serde::Serialize; +use static_urls::StaticUrls; +use tracing::{info, instrument}; +use walkdir::WalkDir; + +pub mod import_map; +mod include_static; +mod static_urls; + +use crate::{ + config::{BuildConfig, RenderTemplateFiles}, + Paths, +}; + +#[instrument(skip(paths, config))] +pub fn build(paths: &Paths<'_>, config: &BuildConfig) -> eyre::Result<()> { + info!("building static site"); + + _ = remove_dir_all(paths.target_dir); + create_dir_all(paths.target_dir).context("cannot create target directory")?; + copy_dir("static", paths.target_dir.join("static")).context("cannot copy static directory")?; + + create_dir_all(paths.target_dir.join("static/wasm")) + .context("cannot create static/wasm directory")?; + copy( + paths.target_wasm_dir.join("haku_wasm.wasm"), + paths.target_dir.join("static/wasm/haku.wasm"), + ) + .context("cannot copy haku.wasm file")?; + + let import_map = ImportMap::generate("".into(), &config.import_roots); + write( + paths.target_dir.join("static/import_map.json"), + serde_json::to_string(&import_map)?, + )?; + + let mut handlebars = Handlebars::new(); + + handlebars.register_helper( + "static", + Box::new(StaticUrls::new( + paths.target_dir.join("static"), + "/static".into(), + )), + ); + handlebars.register_helper( + "include_static", + Box::new(IncludeStatic { + base_dir: paths.target_dir.join("static"), + }), + ); + + for entry in WalkDir::new("template") { + let entry = entry?; + let path = entry.path(); + let file_name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + if file_name + .rsplit_once('.') + .is_some_and(|(left, _)| left.ends_with(".hbs")) + { + handlebars.register_template_file(&file_name, path)?; + info!(file_name, "registered template"); + } + } + + #[derive(Serialize)] + struct SingleFileData {} + + #[derive(Serialize)] + struct DjotData { + title: String, + content: String, + } + + for render_template in &config.render_templates { + info!(?render_template); + match &render_template.files { + RenderTemplateFiles::SingleFile { to_file } => { + let rendered = handlebars.render(&render_template.template, &SingleFileData {})?; + std::fs::write(paths.target_dir.join(to_file), rendered)?; + } + + RenderTemplateFiles::Directory { from_dir, to_dir } => { + create_dir_all(paths.target_dir.join(to_dir))?; + + for entry in WalkDir::new(from_dir) { + let entry = entry?; + let inner_path = entry.path().strip_prefix(from_dir)?; + + if entry.path().extension() == Some(OsStr::new("dj")) { + let djot = std::fs::read_to_string(entry.path())?; + let events = jotdown::Parser::new(&djot); + let content = jotdown::html::render_to_string(events); + let title = config + .page_titles + .get(entry.path()) + .cloned() + .unwrap_or_else(|| entry.path().to_string_lossy().into_owned()); + let rendered = handlebars + .render(&render_template.template, &DjotData { title, content })?; + std::fs::write( + paths + .target_dir + .join(to_dir) + .join(inner_path.with_extension("html")), + rendered, + )?; + } + } + } + } + } + + Ok(()) +} diff --git a/crates/rkgk/src/build/import_map.rs b/crates/rkgk/src/build/import_map.rs new file mode 100644 index 0000000..ca08c1c --- /dev/null +++ b/crates/rkgk/src/build/import_map.rs @@ -0,0 +1,67 @@ +use std::{ffi::OsStr, path::PathBuf}; + +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use tracing::warn; +use walkdir::WalkDir; + +use super::static_urls::StaticUrls; + +#[derive(Debug, Clone, Serialize)] +pub struct ImportMap { + pub imports: IndexMap, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ImportRoot { + pub name: String, + pub path: String, +} + +impl ImportMap { + pub fn generate(base_url: String, import_roots: &[ImportRoot]) -> Self { + let mut import_map = ImportMap { + imports: IndexMap::new(), + }; + + for root in import_roots { + let static_urls = StaticUrls::new( + PathBuf::from(&root.path), + format!("{base_url}/{}", root.path), + ); + for entry in WalkDir::new(&root.path) { + let entry = match entry { + Ok(entry) => entry, + Err(error) => { + warn!("directory walk failed: {error}"); + continue; + } + }; + + if !entry.file_type().is_dir() && entry.path().extension() == Some(OsStr::new("js")) + { + let normalized_path = entry + .path() + .strip_prefix(&root.path) + .unwrap_or(entry.path()) + .to_string_lossy() + .replace('\\', "/"); + match static_urls.get(&normalized_path) { + Ok(url) => { + import_map + .imports + .insert(format!("{}/{normalized_path}", root.name), url); + } + Err(error) => { + warn!("could not get static url for {normalized_path}: {error}") + } + } + } + } + } + + import_map.imports.sort_unstable_keys(); + + import_map + } +} diff --git a/crates/rkgk/src/build/include_static.rs b/crates/rkgk/src/build/include_static.rs new file mode 100644 index 0000000..399a6cc --- /dev/null +++ b/crates/rkgk/src/build/include_static.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use handlebars::{ + Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, RenderErrorReason, + ScopedJson, +}; +use serde_json::Value; + +pub struct IncludeStatic { + pub base_dir: PathBuf, +} + +impl HelperDef for IncludeStatic { + fn call_inner<'reg: 'rc, 'rc>( + &self, + helper: &Helper<'rc>, + _: &'reg Handlebars<'reg>, + _: &'rc Context, + _: &mut RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + if let Some(param) = helper.param(0).and_then(|v| v.value().as_str()) { + return Ok(ScopedJson::Derived(Value::String( + std::fs::read_to_string(self.base_dir.join(param)).map_err(|error| { + RenderErrorReason::Other(format!("cannot read static asset {param}: {error}")) + })?, + ))); + } + + Err(RenderErrorReason::Other("asset path must be provided".into()).into()) + } +} diff --git a/crates/rkgk/src/build/static_urls.rs b/crates/rkgk/src/build/static_urls.rs new file mode 100644 index 0000000..db382a5 --- /dev/null +++ b/crates/rkgk/src/build/static_urls.rs @@ -0,0 +1,79 @@ +use std::{ + collections::HashMap, + fs::File, + io::{self, BufReader}, + path::PathBuf, + sync::RwLock, +}; + +use handlebars::{ + Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, RenderErrorReason, + ScopedJson, +}; +use serde_json::Value; + +pub struct StaticUrls { + base_dir: PathBuf, + base_url: String, + // Really annoying that we have to use an RwLock for this. We only ever generate in a + // single-threaded environment. + // Honestly it would be a lot more efficient if Handlebars just assumed single-threadedness + // and required you to clone it over to different threads. + // Stuff like this is why I really want to implement my own templating engine... + hash_cache: RwLock>, +} + +impl StaticUrls { + pub fn new(base_dir: PathBuf, base_url: String) -> Self { + Self { + base_dir, + base_url, + hash_cache: RwLock::new(HashMap::new()), + } + } + + pub fn get(&self, filename: &str) -> Result { + let hash_cache = self.hash_cache.read().unwrap(); + if let Some(cached) = hash_cache.get(filename) { + return Ok(cached.to_owned()); + } + drop(hash_cache); + + let mut hasher = blake3::Hasher::new(); + let file = BufReader::new(File::open(self.base_dir.join(filename))?); + hasher.update_reader(file)?; + // NOTE: Here the hash is truncated to 8 characters. This is fine, because we don't + // care about security here - only detecting changes in files. + let hash = format!( + "{}/{}?cache=b3-{}", + self.base_url, + filename, + &hasher.finalize().to_hex()[0..8] + ); + { + let mut hash_cache = self.hash_cache.write().unwrap(); + hash_cache.insert(filename.to_owned(), hash.clone()); + } + Ok(hash) + } +} + +impl HelperDef for StaticUrls { + fn call_inner<'reg: 'rc, 'rc>( + &self, + helper: &Helper<'rc>, + _: &'reg Handlebars<'reg>, + _: &'rc Context, + _: &mut RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + if let Some(param) = helper.param(0).and_then(|v| v.value().as_str()) { + return Ok(ScopedJson::Derived(Value::String( + self.get(param).map_err(|error| { + RenderErrorReason::Other(format!("cannot get asset url for {param}: {error}")) + })?, + ))); + } + + Err(RenderErrorReason::Other("asset path must be provided".into()).into()) + } +} diff --git a/crates/rkgk/src/config.rs b/crates/rkgk/src/config.rs index 63a6f03..59667c1 100644 --- a/crates/rkgk/src/config.rs +++ b/crates/rkgk/src/config.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, path::PathBuf}; use serde::{Deserialize, Serialize}; -use crate::wall; +use crate::{build::import_map::ImportRoot, wall}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { @@ -15,6 +15,7 @@ pub struct Config { pub struct BuildConfig { pub render_templates: Vec, pub page_titles: HashMap, + pub import_roots: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -27,5 +28,6 @@ pub struct RenderTemplate { #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum RenderTemplateFiles { + SingleFile { to_file: PathBuf }, Directory { from_dir: PathBuf, to_dir: PathBuf }, } diff --git a/crates/rkgk/src/main.rs b/crates/rkgk/src/main.rs index e31f787..5e1f871 100644 --- a/crates/rkgk/src/main.rs +++ b/crates/rkgk/src/main.rs @@ -1,29 +1,20 @@ -use std::{ - ffi::OsStr, - fs::{copy, create_dir_all, remove_dir_all}, - net::Ipv4Addr, - path::Path, - sync::Arc, -}; +use std::{fs::create_dir_all, net::Ipv4Addr, path::Path, sync::Arc}; use api::Api; -use axum::Router; -use config::{BuildConfig, Config, RenderTemplateFiles}; -use copy_dir::copy_dir; +use config::Config; use eyre::Context; -use handlebars::Handlebars; -use serde::Serialize; +use router::router; use tokio::{fs, net::TcpListener}; -use tower_http::services::{ServeDir, ServeFile}; -use tracing::{info, instrument}; -use walkdir::WalkDir; +use tracing::info; mod api; +mod auto_reload; +mod build; mod config; mod haku; mod id; -mod live_reload; mod login; +mod router; mod schema; mod serialization; mod wall; @@ -33,87 +24,11 @@ mod wall; static GLOBAL_ALLOCATOR: tracy_client::ProfiledAllocator = tracy_client::ProfiledAllocator::new(std::alloc::System, 100); -struct Paths<'a> { - target_dir: &'a Path, - target_wasm_dir: &'a Path, - database_dir: &'a Path, -} - -#[instrument(skip(paths, config))] -fn build(paths: &Paths<'_>, config: &BuildConfig) -> eyre::Result<()> { - info!("building static site"); - - _ = remove_dir_all(paths.target_dir); - create_dir_all(paths.target_dir).context("cannot create target directory")?; - copy_dir("static", paths.target_dir.join("static")).context("cannot copy static directory")?; - - create_dir_all(paths.target_dir.join("static/wasm")) - .context("cannot create static/wasm directory")?; - copy( - paths.target_wasm_dir.join("haku_wasm.wasm"), - paths.target_dir.join("static/wasm/haku.wasm"), - ) - .context("cannot copy haku.wasm file")?; - - let mut handlebars = Handlebars::new(); - for entry in WalkDir::new("template") { - let entry = entry?; - let path = entry.path(); - let file_name = path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .into_owned(); - if file_name - .rsplit_once('.') - .is_some_and(|(left, _)| left.ends_with(".hbs")) - { - handlebars.register_template_file(&file_name, path)?; - info!(file_name, "registered template"); - } - } - - #[derive(Serialize)] - struct DjotData { - title: String, - content: String, - } - - for render_template in &config.render_templates { - info!(?render_template); - match &render_template.files { - RenderTemplateFiles::Directory { from_dir, to_dir } => { - create_dir_all(paths.target_dir.join(to_dir))?; - - for entry in WalkDir::new(from_dir) { - let entry = entry?; - let inner_path = entry.path().strip_prefix(from_dir)?; - - if entry.path().extension() == Some(OsStr::new("dj")) { - let djot = std::fs::read_to_string(entry.path())?; - let events = jotdown::Parser::new(&djot); - let content = jotdown::html::render_to_string(events); - let title = config - .page_titles - .get(entry.path()) - .cloned() - .unwrap_or_else(|| entry.path().to_string_lossy().into_owned()); - let rendered = handlebars - .render(&render_template.template, &DjotData { title, content })?; - std::fs::write( - paths - .target_dir - .join(to_dir) - .join(inner_path.with_extension("html")), - rendered, - )?; - } - } - } - } - } - - Ok(()) +#[derive(Debug, Clone, Copy)] +pub struct Paths<'a> { + pub target_dir: &'a Path, + pub target_wasm_dir: &'a Path, + pub database_dir: &'a Path, } pub struct Databases { @@ -152,20 +67,11 @@ async fn fallible_main() -> eyre::Result<()> { ) .context("cannot deserialize config file")?; - build(&paths, &config.build)?; + build::build(&paths, &config.build)?; let dbs = Arc::new(database(&config, &paths)?); let api = Arc::new(Api { config, dbs }); - let app = Router::new() - .route_service( - "/", - ServeFile::new(paths.target_dir.join("static/index.html")), - ) - .nest_service("/static", ServeDir::new(paths.target_dir.join("static"))) - .nest_service("/docs", ServeDir::new(paths.target_dir.join("docs"))) - .nest("/api", api::router(api)); - - let app = app.nest("/auto-reload", live_reload::router()); + let app = router(&paths, api); let port: u16 = std::env::var("RKGK_PORT") .unwrap_or("8080".into()) diff --git a/crates/rkgk/src/router.rs b/crates/rkgk/src/router.rs new file mode 100644 index 0000000..3824bb0 --- /dev/null +++ b/crates/rkgk/src/router.rs @@ -0,0 +1,97 @@ +use std::{path::PathBuf, sync::Arc}; + +use axum::{ + extract::{Path, Query, State}, + http::{ + header::{CACHE_CONTROL, CONTENT_TYPE}, + HeaderValue, + }, + response::{Html, IntoResponse, Response}, + routing::get, + Router, +}; +use serde::Deserialize; + +use crate::{ + api::{self, Api}, + auto_reload, Paths, +}; + +struct Server { + target_dir: PathBuf, + + index_html: String, + four_oh_four_html: String, +} + +pub fn router(paths: &Paths, api: Arc) -> Router { + Router::new() + .route("/", get(index)) + .route("/static/*path", get(static_file)) + .route("/docs/*path", get(docs)) + .nest("/api", api::router(api)) + .nest("/auto-reload", auto_reload::router()) + .fallback(get(four_oh_four)) + .with_state(Arc::new(Server { + target_dir: paths.target_dir.to_path_buf(), + + index_html: std::fs::read_to_string(paths.target_dir.join("static/index.html")) + .expect("index.html does not exist"), + four_oh_four_html: std::fs::read_to_string(paths.target_dir.join("static/404.html")) + .expect("404.html does not exist"), + })) +} + +async fn index(State(state): State>) -> Html { + Html(state.index_html.clone()) +} + +async fn four_oh_four(State(state): State>) -> Html { + Html(state.four_oh_four_html.clone()) +} + +#[derive(Deserialize)] +struct StaticFileQuery { + cache: Option, +} + +async fn static_file( + Path(path): Path, + Query(query): Query, + State(state): State>, +) -> Response { + if let Ok(file) = tokio::fs::read(state.target_dir.join("static").join(&path)).await { + let mut response = file.into_response(); + + if let Some(content_type) = mime_guess::from_path(&path).first_raw() { + response + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static(content_type)); + } else { + response.headers_mut().remove(CONTENT_TYPE); + } + + if query.cache.is_some() { + response.headers_mut().insert( + CACHE_CONTROL, + HeaderValue::from_static("public, max-age=31536000, immutable"), + ); + } + + response + } else { + four_oh_four(State(state)).await.into_response() + } +} + +async fn docs(Path(mut path): Path, state: State>) -> Html { + if !path.ends_with(".html") { + path.push_str(".html") + } + + if let Ok(file) = tokio::fs::read_to_string(state.target_dir.join("docs").join(&path)).await { + Html(file) + } else { + four_oh_four(state).await + } +} diff --git a/rkgk.toml b/rkgk.toml index ef0107d..1c2b2ce 100644 --- a/rkgk.toml +++ b/rkgk.toml @@ -4,7 +4,15 @@ # List of Handlebars templates to render. render_templates = [ - { template = "docs.hbs.html", from_dir = "docs", to_dir = "docs" } + { template = "fonts.hbs.css", to_file = "static/fonts.css" }, + + { template = "index.hbs.html", to_file = "static/index.html" }, + { template = "docs.hbs.html", from_dir = "docs", to_dir = "docs" }, +] + +# List of JavaScript `import` root directories. +import_roots = [ + { name = "rkgk", path = "static" }, ] [build.page_titles] diff --git a/static/404.html b/static/404.html new file mode 100644 index 0000000..b5162c3 --- /dev/null +++ b/static/404.html @@ -0,0 +1,17 @@ + + + + + + + 404 Not Found + + + +

404 Not Found

+

It appears the thing you're looking for doesn't exist.

+

Back to rakugaki

+ + + + diff --git a/static/base.css b/static/base.css index b1d0680..5330e5b 100644 --- a/static/base.css +++ b/static/base.css @@ -23,50 +23,6 @@ body { /* Fonts */ -@font-face { - font-family: "Atkinson Hyperlegible"; - src: - local("Atkinson Hyperlegible Regular"), - url("font/AtkinsonHyperlegible-Regular.ttf"); - font-weight: 400; -} - -@font-face { - font-family: "Atkinson Hyperlegible"; - src: - local("Atkinson Hyperlegible Italic"), - url("font/AtkinsonHyperlegible-Italic.ttf"); - font-weight: 400; - font-style: italic; -} - -@font-face { - font-family: "Atkinson Hyperlegible"; - src: - local("Atkinson Hyperlegible Bold"), - url("font/AtkinsonHyperlegible-Bold.ttf"); - font-weight: 700; -} - -@font-face { - font-family: "Atkinson Hyperlegible"; - src: - local("Atkinson Hyperlegible Bold Italic"), - url("font/AtkinsonHyperlegible-BoldItalic.ttf"); - font-weight: 700; - font-style: italic; -} - -@font-face { - /* NOTE: This is my own variant of Iosevka that more or less follows the stylistic choices - of Atkinson Hyperlegible. */ - font-family: "Iosevka Hyperlegible"; - src: - local("Iosevka Hyperlegible"), - url("font/IosevkaHyperlegible-Regular.woff2"); - font-weight: 400; -} - :root { font-family: "Atkinson Hyperlegible", sans-serif; } diff --git a/static/canvas-renderer.js b/static/canvas-renderer.js index 7a5c62d..9e48807 100644 --- a/static/canvas-renderer.js +++ b/static/canvas-renderer.js @@ -1,6 +1,6 @@ -import { listen } from "./framework.js"; -import { Viewport } from "./viewport.js"; -import { Wall } from "./wall.js"; +import { listen } from "rkgk/framework.js"; +import { Viewport } from "rkgk/viewport.js"; +import { Wall } from "rkgk/wall.js"; class CanvasRenderer extends HTMLElement { viewport = new Viewport(); diff --git a/static/haku.js b/static/haku.js index d3f8b82..1392cbe 100644 --- a/static/haku.js +++ b/static/haku.js @@ -8,7 +8,7 @@ function makeLogFunction(level) { } let { instance: hakuInstance, module: hakuModule } = await WebAssembly.instantiateStreaming( - fetch(import.meta.resolve("./wasm/haku.wasm")), + fetch(HAKU_WASM_PATH), { env: { panic(length, pMessage) { diff --git a/static/index.js b/static/index.js index f3a6c07..f4c3c56 100644 --- a/static/index.js +++ b/static/index.js @@ -1,4 +1,4 @@ -import { Wall } from "./wall.js"; +import { Wall } from "rkgk/wall.js"; import { getLoginSecret, getUserId, @@ -6,9 +6,9 @@ import { newSession, registerUser, waitForLogin, -} from "./session.js"; -import { debounce } from "./framework.js"; -import { ReticleCursor } from "./reticle-renderer.js"; +} from "rkgk/session.js"; +import { debounce } from "rkgk/framework.js"; +import { ReticleCursor } from "rkgk/reticle-renderer.js"; const updateInterval = 1000 / 60; diff --git a/static/online-users.js b/static/online-users.js index dc78637..016e572 100644 --- a/static/online-users.js +++ b/static/online-users.js @@ -1,5 +1,5 @@ -import { Haku } from "./haku.js"; -import { Painter } from "./painter.js"; +import { Haku } from "rkgk/haku.js"; +import { Painter } from "rkgk/painter.js"; export class User { nickname = ""; diff --git a/static/resize-handle.js b/static/resize-handle.js index 9a11b6c..729bb13 100644 --- a/static/resize-handle.js +++ b/static/resize-handle.js @@ -1,4 +1,4 @@ -import { listen } from "./framework.js"; +import { listen } from "rkgk/framework.js"; export class ResizeHandle extends HTMLElement { constructor() { diff --git a/static/session.js b/static/session.js index 36bec0c..9558b29 100644 --- a/static/session.js +++ b/static/session.js @@ -1,4 +1,4 @@ -import { listen } from "./framework.js"; +import { listen } from "rkgk/framework.js"; let loginStorage = JSON.parse(localStorage.getItem("rkgk.login") ?? "{}"); diff --git a/static/wall.js b/static/wall.js index f69929e..30d051f 100644 --- a/static/wall.js +++ b/static/wall.js @@ -1,5 +1,5 @@ -import { Pixmap } from "./haku.js"; -import { OnlineUsers } from "./online-users.js"; +import { Pixmap } from "rkgk/haku.js"; +import { OnlineUsers } from "rkgk/online-users.js"; export class Chunk { constructor(size) { diff --git a/template/docs.hbs.html b/template/docs.hbs.html index c147f3d..cb83f91 100644 --- a/template/docs.hbs.html +++ b/template/docs.hbs.html @@ -4,12 +4,13 @@ {{ title }} ยท rakugaki manual - - + + + - + diff --git a/template/fonts.hbs.css b/template/fonts.hbs.css new file mode 100644 index 0000000..84aa5b7 --- /dev/null +++ b/template/fonts.hbs.css @@ -0,0 +1,44 @@ +@font-face { + font-family: "Atkinson Hyperlegible"; + src: + local("Atkinson Hyperlegible Regular"), + url("{{ static 'font/AtkinsonHyperlegible-Regular.ttf' }}"); + font-weight: 400; +} + +@font-face { + font-family: "Atkinson Hyperlegible"; + src: + local("Atkinson Hyperlegible Italic"), + url("{{ static 'font/AtkinsonHyperlegible-Italic.ttf' }}"); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: "Atkinson Hyperlegible"; + src: + local("Atkinson Hyperlegible Bold"), + url("{{ static 'font/AtkinsonHyperlegible-Bold.ttf' }}"); + font-weight: 700; +} + +@font-face { + font-family: "Atkinson Hyperlegible"; + src: + local("Atkinson Hyperlegible Bold Italic"), + url("{{ static 'font/AtkinsonHyperlegible-BoldItalic.ttf' }}"); + font-weight: 700; + font-style: italic; +} + +@font-face { + /* NOTE: This is my own variant of Iosevka that more or less follows the stylistic choices + of Atkinson Hyperlegible. */ + font-family: "Iosevka Hyperlegible"; + src: + local("Iosevka Hyperlegible"), + url("{{ static 'font/IosevkaHyperlegible-Regular.woff2' }}"); + font-weight: 400; +} + diff --git a/static/index.html b/template/index.hbs.html similarity index 69% rename from static/index.html rename to template/index.hbs.html index cfdda0b..03fd22c 100644 --- a/static/index.html +++ b/template/index.hbs.html @@ -6,38 +6,47 @@ rakugaki - - + + + - - - - - - - - - - - - - - + - - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -133,3 +142,4 @@ +