diff --git a/Cargo.lock b/Cargo.lock index cf0cf9b..183d28a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.3.2" @@ -443,6 +452,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -596,6 +614,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.9" @@ -653,6 +677,16 @@ dependencies = [ "want", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -816,6 +850,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.5.0" @@ -824,13 +864,17 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "meowy-webring" -version = "0.1.0" +version = "0.2.0" dependencies = [ "askama", "askama_rocket", + "hex", "log", + "md5", "notify", + "proc_macros", "rocket", + "rocket_cors", "rust-embed", "serde", "serde_json", @@ -1067,6 +1111,14 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc_macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.28" @@ -1161,6 +1213,8 @@ version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax 0.7.2", ] @@ -1240,6 +1294,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "rocket_cors" +version = "0.6.0-alpha2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12771b47f52e34d5d0e0e444aeba382863e73263cb9e18847e7d5b74aa2cbd0" +dependencies = [ + "http", + "log", + "regex", + "rocket", + "unicase", + "url", +] + [[package]] name = "rocket_http" version = "0.5.0-rc.3" @@ -1579,6 +1647,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.29.0" @@ -1775,18 +1858,44 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 79e9b30..a0b6478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,27 @@ [workspace] -members = ["cli", "shared"] +members = ["cli", "shared", "proc-macros"] [package] name = "meowy-webring" -version = "0.1.0" +version = "0.2.0" edition = "2021" rust-version = "1.70" +[profile.dev] +lto = false +[profile.release] +lto = "thin" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +hex = "0.4" log = "0.4" +md5 = "0.7" [dependencies.rocket] version = "=0.5.0-rc.3" -default_features = false +default-features = false features = ["json"] [dependencies.rust-embed] @@ -51,3 +58,10 @@ default-features = false version = "6" default-features = false features = ["macos_fsevent"] + +[dependencies.rocket_cors] +version = "=0.6.0-alpha2" +default_features = false + +[dependencies.proc_macros] +path = "./proc-macros" diff --git a/LICENSE b/LICENSE index 0e259d4..98d0b28 100644 --- a/LICENSE +++ b/LICENSE @@ -2,14 +2,14 @@ Creative Commons Legal Code CC0 1.0 Universal - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. Statement of Purpose @@ -43,22 +43,22 @@ Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; + communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; + likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; + subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; + in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. + world based on applicable law or treaty, and any national + implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, @@ -102,20 +102,20 @@ express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. + surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/proc-macros/Cargo.toml b/proc-macros/Cargo.toml new file mode 100644 index 0000000..27444fd --- /dev/null +++ b/proc-macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "proc_macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +syn = "2.0" +quote = "1.0" diff --git a/proc-macros/src/lib.rs b/proc-macros/src/lib.rs new file mode 100644 index 0000000..b6dcb4e --- /dev/null +++ b/proc-macros/src/lib.rs @@ -0,0 +1,35 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse::Parser, parse_macro_input, DeriveInput}; + +#[proc_macro_attribute] +pub fn uses_base_template(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input = parse_macro_input!(item as DeriveInput); + + let base_template_field = syn::Field::parse_named + .parse2( + quote! { + pub base_template: BaseTemplate + } + .into(), + ) + .unwrap(); + + match &mut input.data { + syn::Data::Struct(ref mut struct_data) => { + match &mut struct_data.fields { + syn::Fields::Named(fields) => { + fields.named.push(base_template_field); + } + _ => (), + } + return quote! { + #input + } + .into(); + } + _ => { + panic!("bad") + } + }; +} diff --git a/public/hyperlegible.css b/public/hyperlegible.css new file mode 100644 index 0000000..eca87f6 --- /dev/null +++ b/public/hyperlegible.css @@ -0,0 +1,43 @@ +@font-face { + font-family: Atkinson Hyperlegible; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(/public/woff2/atkinson-hyperlegible-latin-ext-400-normal.woff2) format("woff2"), + url(/public/woff/atkinson-hyperlegible-all-400-normal.woff) format("woff"); + unicode-range: U+0100-024F, + U+0259, + U+1E00-1EFF, + U+2020, + U+20A0-20AB, + U+20AD-20CF, + U+2113, + U+2C60-2C7F, + U+A720-A7FF +} + +@font-face { + font-family: Atkinson Hyperlegible; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(/public/woff2/atkinson-hyperlegible-latin-400-normal.woff2) format("woff2"), + url(/public/woff/atkinson-hyperlegible-all-400-normal.woff) format("woff"); + unicode-range: U+0000-00FF, + U+0131, + U+0152-0153, + U+02BB-02BC, + U+02C6, + U+02DA, + U+02DC, + U+2000-206F, + U+2074, + U+20AC, + U+2122, + U+2191, + U+2193, + U+2212, + U+2215, + U+FEFF, + U+FFFD +} diff --git a/public/style.css b/public/style.css index b1801cb..2674a9d 100644 --- a/public/style.css +++ b/public/style.css @@ -14,59 +14,51 @@ } } +:root { + --h1-font-size: 3.225rem; + --h2-font-size: 2.825rem; + --h3-font-size: 2.225rem; + --h4-font-size: 1.665rem; + --default-font-size: 1.375rem; + --h6-font-size: 1.185rem; +} + body { font-family: "Atkinson Hyperlegible", sans-serif; text-align: center; + font-size: var(--default-font-size); max-width: 600px; margin: auto; background-color: var(--background-color); color: var(--text-color) } -p { - font-size: 22px; + +a { + font-size: var(--default-font-size); + color: var(--link-color); } -@font-face { - font-family: Atkinson Hyperlegible; - font-style: normal; - font-display: swap; - font-weight: 400; - src: url(/public/woff2/atkinson-hyperlegible-latin-ext-400-normal.woff2) format("woff2"), - url(/public/woff/atkinson-hyperlegible-all-400-normal.woff) format("woff"); - unicode-range: U+0100-024F, - U+0259, - U+1E00-1EFF, - U+2020, - U+20A0-20AB, - U+20AD-20CF, - U+2113, - U+2C60-2C7F, - U+A720-A7FF +h1 { + font-size: var(--h1-font-size); } -@font-face { - font-family: Atkinson Hyperlegible; - font-style: normal; - font-display: swap; - font-weight: 400; - src: url(/public/woff2/atkinson-hyperlegible-latin-400-normal.woff2) format("woff2"), - url(/public/woff/atkinson-hyperlegible-all-400-normal.woff) format("woff"); - unicode-range: U+0000-00FF, - U+0131, - U+0152-0153, - U+02BB-02BC, - U+02C6, - U+02DA, - U+02DC, - U+2000-206F, - U+2074, - U+20AC, - U+2122, - U+2191, - U+2193, - U+2212, - U+2215, - U+FEFF, - U+FFFD +h2 { + font-size: var(--h2-font-size); +} + +h3 { + font-size: var(--h3-font-size); +} + +h4 { + font-size: var(--h4-font-size); +} + +h5 { + font-size: var(--default-font-size); +} + +h6 { + font-size: var(--h6-font-size); } diff --git a/src/assets.rs b/src/assets.rs deleted file mode 100644 index 35ea3a0..0000000 --- a/src/assets.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::borrow::Cow; - -use askama_rocket::Template; -use rocket::http::Status; -use rust_embed::RustEmbed; - -#[derive(RustEmbed)] -#[folder = "public/"] -pub struct PublicAssets; - -#[derive(Responder)] -#[response(status = 200, content_type = "font/woff2")] -pub struct RawWoff2Font(pub Cow<'static, [u8]>); - -#[derive(Responder)] -#[response(status = 200, content_type = "font/woff")] -pub struct RawWoffFont(pub Cow<'static, [u8]>); - -#[derive(Template)] -#[template(path = "error.html")] -pub struct ErrorTemplate<'a> { - pub error: &'a str, - pub error_description: &'a str -} - -#[derive(Responder)] -pub struct ErrorTemplateResponder<'a> { - template: ErrorTemplate<'a> -} - -#[get("/style.css")] -pub fn style() -> Result, Status> { - let style = PublicAssets::get("style.css").unwrap(); - match std::str::from_utf8(&style.data) { - Ok(style) => Ok(rocket::response::content::RawCss::(style.to_string())), - Err(_) => Err(Status::InternalServerError), - } -} - -#[get("/woff2/")] -pub fn woff2_font(font: &str) -> Result { - let latin = "atkinson-hyperlegible-latin-400-normal.woff2"; - let latin_ext = "atkinson-hyperlegible-latin-ext-400-normal.woff2"; - - if font == latin { - Ok(RawWoff2Font(PublicAssets::get(latin).unwrap().data)) - } else if font == latin_ext { - Ok(RawWoff2Font(PublicAssets::get(latin_ext).unwrap().data)) - } else { - Err(Status::NotFound) - } -} - -#[get("/woff/")] -pub fn woff_font(font: &str) -> Result { - let all = "atkinson-hyperlegible-all-400-normal.woff"; - - if font == all { - Ok(RawWoffFont(PublicAssets::get(all).unwrap().data)) - } else { - Err(Status::NotFound) - } -} diff --git a/src/assets/files.rs b/src/assets/files.rs new file mode 100644 index 0000000..f09e22e --- /dev/null +++ b/src/assets/files.rs @@ -0,0 +1,96 @@ +use rocket::http::Status; +use rust_embed::RustEmbed; +use std::{borrow::Cow, sync::OnceLock}; + +#[derive(RustEmbed)] +#[folder = "public/"] +struct Assets; + +#[derive(Debug)] +pub struct Files { + pub atkinson_latin_woff2: BinaryFile, + pub atkinson_latin_ext_woff2: BinaryFile, + pub atkinson_all_woff: BinaryFile, + pub style: TextFile, + pub hyperlegible: TextFile, +} + +#[derive(Debug, Clone)] +pub struct FileMetadata { + pub filename: String, + pub extension: String, + pub hash: String, +} + +impl FileMetadata { + pub fn get_hash_filename(&self) -> String { + let mut hash = self.hash.clone(); + hash.truncate(8); + format!("{}.{}.{}", self.filename, hash, self.extension) + } +} + +#[derive(Debug)] +pub struct BinaryFile { + pub data: Cow<'static, [u8]>, + pub metadata: FileMetadata, +} + +#[derive(Debug, Clone)] +pub struct TextFile { + pub text: String, + pub metadata: FileMetadata, +} + +pub static FILES: OnceLock = OnceLock::new(); + +pub fn get_file_wrapper() -> Result<&'static Files, Status> { + match FILES.get() { + Some(files) => Ok(files), + None => Err(Status::InternalServerError), + } +} + +fn get_binary_file(filename: &str, extension: &str) -> Option { + match Assets::get(&format!("{}.{}", filename, extension)) { + Some(file) => { + let metadata = FileMetadata { + filename: filename.into(), + extension: extension.into(), + hash: hex::encode(file.metadata.sha256_hash()), + }; + Some(BinaryFile { + data: file.data, + metadata: metadata, + }) + } + None => None, + } +} + +fn get_text_file(filename: &str, extension: &str) -> Option { + let file = get_binary_file(filename, extension)?; + match std::str::from_utf8(&file.data) { + Ok(string) => Some(TextFile { + text: string.into(), + metadata: file.metadata, + }), + Err(_) => None, + } +} + +pub fn initialize_files() { + let files = Files { + atkinson_latin_woff2: get_binary_file("atkinson-hyperlegible-latin-400-normal", "woff2") + .unwrap(), + atkinson_latin_ext_woff2: get_binary_file( + "atkinson-hyperlegible-latin-ext-400-normal", + "woff2", + ) + .unwrap(), + atkinson_all_woff: get_binary_file("atkinson-hyperlegible-all-400-normal", "woff").unwrap(), + style: get_text_file("style", "css").unwrap(), + hyperlegible: get_text_file("hyperlegible", "css").unwrap(), + }; + FILES.set(files).unwrap(); +} diff --git a/src/assets/mod.rs b/src/assets/mod.rs new file mode 100644 index 0000000..2e4d45e --- /dev/null +++ b/src/assets/mod.rs @@ -0,0 +1,7 @@ +pub mod files; +mod routes; +pub mod templates; + +pub use routes::style; +pub use routes::woff2_font; +pub use routes::woff_font; diff --git a/src/assets/routes.rs b/src/assets/routes.rs new file mode 100644 index 0000000..db2be30 --- /dev/null +++ b/src/assets/routes.rs @@ -0,0 +1,96 @@ +use super::{files::get_file_wrapper, templates::ErrorTemplate}; +use rocket::{ + http::{Header, Status}, + response::{self, content::RawCss, Responder}, + Response, +}; +use std::borrow::Cow; + +#[derive(Responder)] +#[response(status = 200, content_type = "font/woff2")] +pub struct RawWoff2Font(pub Cow<'static, [u8]>); + +#[derive(Responder)] +#[response(status = 200, content_type = "font/woff")] +pub struct RawWoffFont(pub Cow<'static, [u8]>); + +#[derive(Responder)] +pub struct ErrorTemplateResponder<'a> { + template: ErrorTemplate<'a>, +} + +pub struct CachedResponse { + inner: T, +} + +impl<'r, T> Responder<'r, 'static> for CachedResponse +where + T: Responder<'r, 'static>, +{ + fn respond_to(self, request: &'r rocket::Request<'_>) -> response::Result<'static> { + Response::build_from(self.inner.respond_to(request)?) + .header(Header::new("Cache-Control", "max-age=31536000, immutable")) + .ok() + } +} + +impl<'r, T> From for CachedResponse +where + T: Responder<'r, 'static>, +{ + fn from(value: T) -> Self { + CachedResponse { inner: value } + } +} + +#[get("/css/ + + {% endblock %} + + + {% block content %} + {% endblock %} + + diff --git a/templates/error.html b/templates/error.html index d02027b..b35dfa3 100644 --- a/templates/error.html +++ b/templates/error.html @@ -1,15 +1,10 @@ - - - - - - Meowy Webring - {{ error }} - - - -
-

{{ error }}

-

{{ error_description }}

-
- - +{% extends "base.html" %} + +{% block title %} - {{ error }}{% endblock %} + +{% block content %} +
+

{{ error }}

+

{{ error_description }}

+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..51fd364 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Meowy Webring

+

Sites

+ {% for site in sites %} + {% match site.name %} + {% when Some with (value) %} +

{{ value }}

+ {% when None %} +

{{ site.url }}

+ {% endmatch %} + {% endfor %} +
+{% endblock %}