The parts every Jev page is built from: the nav, the headline, the window, the pink band, the footer and the copy line. They return markup and know nothing of a site's content.
4use maud::{Markup, html};
A 12 by 12 pixel icon: # for ink, anything else for paper.
7pub type Icon = [&'static str; 12];
The icons lmjtfy's title bars use; a site adds its own.
10pub mod icons { 11 use super::Icon; 12 13 pub const ASK: Icon = [ 14 "............", "..#####.....", ".#.....#....", ".#.....#....", ".#.....#....", ".#.....#....", "..#####.....", ".......##...", 15 "........##..", ".........##.", "..........#.", "............", 16 ]; 17 pub const JEV: Icon = [ 18 ".....##.....", "....#.##....", "...#.#.##...", "..#.#.#.##..", ".#.#.#.#.##.", "#.#.#.#.####", "####.#.#.#.#", ".##.#.#.#.#.", 19 "..##.#.#.#..", "...##.#.#...", "....##.#....", ".....##.....", 20 ]; 21 pub const LLM: Icon = [ 22 "............", ".#########..", ".#.......#..", ".#.##.##.#..", ".#.......#..", ".#.#####.#..", ".#.......#..", ".#########..", 23 "...##.......", "..##........", ".#..........", "............", 24 ]; 25 pub const TOOL: Icon = [ 26 "............", ".......###..", "......##....", "......##..#.", "......#####.", ".....####...", "....###.....", "...###......", 27 "..###.......", ".###........", ".##.........", "............", 28 ]; 29}
A pixel icon as inline SVG: one square per #. class is added after icon (big for the
framed one in a footer).
33pub fn pixel_icon(rows: &Icon, class: &str) -> Markup { 34 html! { 35 svg class={ "icon " (class) } viewBox="0 0 12 12" aria-hidden="true" { 36 @for (y, row) in rows.iter().enumerate() { 37 @for (x, cell) in row.bytes().enumerate() { 38 @if cell == b'#' { rect x=(x) y=(y) width="1" height="1" fill="currentColor" {} } 39 } 40 } 41 } 42 } 43}
A pixel emoji from SerenityOS's set, the four ui.css has a font for. A glyph the font lacks
cannot be asked for: this is the list, and a test holds it to the stylesheet's unicode-range.
A grey disc with a white triangle: start.
50 Play,
A grey disc with two white bars: stop for now.
52 Pause,
Hourglass with the sand still in the top.
54 SandAtTop,
59impl PixelEmoji { 60 pub const ALL: [PixelEmoji; 4] = [PixelEmoji::Play, PixelEmoji::Pause, PixelEmoji::SandAtTop, PixelEmoji::SandAtBottom]; 61 62 pub const fn glyph(self) -> char { 63 match self { 64 PixelEmoji::Play => '\u{25B6}', 65 PixelEmoji::Pause => '\u{23F8}', 66 PixelEmoji::SandAtTop => '\u{23F3}', 67 PixelEmoji::SandAtBottom => '\u{231B}', 68 } 69 } 70}
A pixel emoji in the text. It is decoration: the words beside it say what it means, so it is hidden from a screen reader.
An hourglass that turns over while something runs (ui.css: still for a reader who asks for
less motion). Hidden from a screen reader; say in words that something is running.
A link in the nav.
96pub struct Nav<'a> {
What the nav is, for a screen reader.
98 pub label: &'a str,
The site's name, at the left.
100 pub brand: Link<'a>,
Anything the site puts between the name and the links (lmjtfy's online count).
102 pub middle: Markup,
The big title with crop marks round it.
A window: a black title bar (an icon, a name, and meta at the right) over a framed body.
title is markup so a section's window can make it its heading (h2, which the bar styles
as its own text). white is a paper body; otherwise it is the grey of the window.
130pub fn window(icon: Option<&Icon>, title: Markup, meta: Option<Markup>, white: bool, body: Markup) -> Markup { 131 html! { 132 div .win.white[white] { 133 div .bar-title { 134 @if let Some(icon) = icon { (pixel_icon(icon, "")) } 135 (title) 136 @if let Some(meta) = meta { span .meta { (meta) } } 137 } 138 div .body { (body) } 139 } 140 } 141}
A white section of the page: the content in a column.
The full-width pink band, its content in the same column.
A command or link with a button that copies it (ui.js, data-copy). id names the <code>.
The foot of a page: what the site is, and optionally how to get its code.
The link that lets a keyboard skip the nav.
184#[cfg(test)] 185mod tests { 186 use super::*; 187 188 #[test] 189 fn an_icon_is_one_square_per_ink_pixel() { 190 let svg = pixel_icon(&icons::JEV, "big").into_string(); 191 let ink: usize = icons::JEV.iter().map(|row| row.matches('#').count()).sum(); 192 assert_eq!(svg.matches("<rect").count(), ink); 193 assert!(svg.contains("class=\"icon big\"") && svg.contains("aria-hidden=\"true\"")); 194 } 195 196 #[test] 197 fn every_icon_is_twelve_by_twelve() { 198 for icon in [icons::ASK, icons::JEV, icons::LLM, icons::TOOL] { 199 assert!(icon.iter().all(|row| row.len() == 12)); 200 } 201 } 202 203 #[test] 204 fn every_pixel_emoji_is_in_the_fonts_range_and_nothing_else_is() { 205 let css = crate::asset::CSS.text(); 206 let range = css.split("unicode-range:").nth(1).unwrap().split(';').next().unwrap(); 207 let listed: Vec<String> = range.split(',').map(|r| r.trim().to_owned()).collect(); 208 assert_eq!(listed.len(), PixelEmoji::ALL.len(), "{range}"); 209 for emoji in PixelEmoji::ALL { 210 assert!(listed.contains(&format!("U+{:X}", emoji.glyph() as u32)), "{emoji:?}"); 211 } 212 } 213 214 #[test] 215 fn a_pixel_emoji_is_decoration_and_the_hourglass_has_both_halves_of_its_turn() { 216 assert_eq!(pixel_emoji(PixelEmoji::Play).into_string(), "<span class=\"pe\" aria-hidden=\"true\">\u{25B6}</span>"); 217 let glass = hourglass().into_string(); 218 assert!(glass.contains("aria-hidden=\"true\"") && glass.contains("pe top") && glass.contains("pe bottom")); 219 assert!(glass.contains('\u{23F3}') && glass.contains('\u{231B}')); 220 } 221 222 #[test] 223 fn the_nav_puts_the_name_first_and_the_dark_links_after() { 224 let links = [Link { href: "#a", label: "A" }, Link { href: "#b", label: "B" }]; 225 let page = nav(&Nav { label: "On this page", brand: Link { href: "/", label: "Site" }, middle: html! { span #mid {} }, links: &links }).into_string(); 226 let (brand, mid, a) = (page.find(">Site<").unwrap(), page.find("id=\"mid\"").unwrap(), page.find("class=\"dark\" href=\"#a\"").unwrap()); 227 assert!(brand < mid && mid < a); 228 assert!(page.contains("aria-label=\"On this page\"")); 229 } 230 231 #[test] 232 fn a_window_has_a_title_bar_over_a_body_and_white_is_a_class() { 233 let html = window(Some(&icons::ASK), html! { h2 { "Ask" } }, Some(html! { "meta" }), true, html! { p { "inside" } }).into_string(); 234 assert!(html.starts_with("<div class=\"win white\">") && html.contains("<span class=\"meta\">meta</span>")); 235 assert!(html.contains("<h2>Ask</h2>")); 236 assert!(html.find("bar-title").unwrap() < html.find("class=\"body\"").unwrap()); 237 assert!(window(None, html! { "x" }, None, false, html! {}).into_string().starts_with("<div class=\"win\">")); 238 } 239 240 #[test] 241 fn the_copy_button_names_the_text_it_copies() { 242 let html = copy_line("clone", html! { "Get it:" }, "git clone x").into_string(); 243 assert!(html.contains("<code id=\"clone\">git clone x</code>") && html.contains("data-copy=\"clone\"")); 244 } 245 246 #[test] 247 fn text_is_escaped() { 248 let html = headline(Some("<i>"), "a & b").into_string(); 249 assert!(html.contains("<i>") && html.contains("a & b")); 250 } 251}