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 @@ +