1 //! Logo suite (m6, f0rge issues #15 + #26 + #27 + #28):2 //! `static/logo.svg` (lockup with hu13 as <path>) + `static/favicon.svg`3 //! (simplified mark-only 16px variant) exist + valid XML + <2KB total,4 //! wordmark variant, owner block-art banner (`static/ascii.txt` +5 //! `static/logo-ansi.txt`, 12 lines) + purity/strip tests + `<pre>`6 //! rendering invariant.7 //!8 //! File-level suite (hu13 is a binary crate, so `src/logo.rs` unit tests9 //! cover the `ascii_pre()` helper itself; here we assert on disk).10 11 use std::path::PathBuf;12 13 fn manifest() -> PathBuf {14 PathBuf::from(env!("CARGO_MANIFEST_DIR"))15 }16 17 fn read_static(name: &str) -> String {18 let path = manifest().join("static").join(name);19 std::fs::read_to_string(&path).unwrap_or_else(|_| panic!("static/{name} exists"))20 }21 22 /// Minimal well-formedness check: balanced tags, quoted attributes, single23 /// root; skips comments, PIs and `<!...>` declarations. Enough to call the24 /// hand-written mark "valid XML" without a new dependency.25 fn check_well_formed(xml: &str) {26 let bytes = xml.as_bytes();27 let mut i = 0;28 let mut stack: Vec<String> = Vec::new();29 let mut root_done = false;30 while i < bytes.len() {31 if bytes[i] != b'<' {32 i += 1;33 continue;34 }35 let rest = &xml[i..];36 if rest.starts_with("<!--") {37 let end = rest.find("-->").expect("unterminated comment");38 i += end + 4;39 continue;40 }41 if rest.starts_with("<?") {42 let end = rest.find("?>").expect("unterminated PI");43 i += end + 2;44 continue;45 }46 if rest.starts_with("<!") {47 let end = rest.find('>').expect("unterminated declaration");48 i += end + 1;49 continue;50 }51 // Tag end, respecting quoted `>` inside attributes.52 let mut j = i + 1;53 let mut quote: Option<u8> = None;54 while j < bytes.len() {55 match (quote, bytes[j]) {56 (Some(q), c) if c == q => quote = None,57 (None, b'"' | b'\'') => quote = Some(bytes[j]),58 (None, b'>') => break,59 _ => {}60 }61 j += 1;62 }63 assert!(j < bytes.len(), "unterminated tag near {rest:?}");64 let tag = &xml[i..=j];65 if let Some(close) = tag.strip_prefix("</") {66 let name = close.trim_end_matches('>').trim().to_string();67 let open = stack.pop().expect("closing tag without opener");68 assert_eq!(open, name, "mismatched tag: <{open}> vs </{name}>");69 if stack.is_empty() {70 root_done = true;71 }72 } else {73 let self_close = tag.ends_with("/>");74 let mut inner = &tag[1..tag.len() - 1];75 inner = inner.strip_suffix('/').unwrap_or(inner);76 let name: &str = inner77 .split(|c: char| c.is_whitespace() || c == '/')78 .next()79 .unwrap_or("");80 assert!(!name.is_empty(), "empty tag name in {tag:?}");81 assert!(82 name.bytes()83 .next()84 .is_some_and(|c| c.is_ascii_alphabetic() || c == b'_' || c == b':'),85 "bad tag name {name:?}"86 );87 if self_close {88 if stack.is_empty() {89 root_done = true;90 }91 } else {92 assert!(!root_done, "second root element <{name}>");93 stack.push(name.to_string());94 }95 }96 i = j + 1;97 }98 assert!(stack.is_empty(), "unclosed tags: {stack:?}");99 assert!(root_done, "no root element");100 }101 102 fn assert_sharp_no_gradient(name: &str, svg: &str) {103 let lower = svg.to_lowercase();104 for bad in [105 "rx=",106 "ry=",107 "<circle",108 "<ellipse",109 "border-radius",110 "linecap=\"round\"",111 "linejoin=\"round\"",112 ] {113 assert!(114 !lower.contains(bad),115 "{name}: no rounded corners ({bad}), sharp geometry only"116 );117 }118 assert!(119 !lower.contains("gradient"),120 "{name}: no gradients (must stay legible at 16px)"121 );122 }123 124 #[test]125 fn logo_svg_exists_valid_tiny() {126 let path = manifest().join("static").join("logo.svg");127 let len = std::fs::metadata(&path)128 .expect("static/logo.svg exists")129 .len();130 assert!(len < 2048, "logo.svg is {len} bytes, budget is 2KB");131 assert!(len > 100, "logo.svg is {len} bytes, suspiciously small");132 let svg = read_static("logo.svg");133 check_well_formed(&svg);134 assert!(svg.contains("<svg"), "has an <svg> root");135 assert!(136 svg.contains("http://www.w3.org/2000/svg"),137 "svg namespace present"138 );139 assert!(svg.contains("viewBox"), "scales via viewBox (16px + 32px)");140 for color in ["#7aa2f7", "#7dcfff", "#c0caf5"] {141 assert!(svg.contains(color), "Tokyo Night palette keeps {color}");142 }143 assert!(144 !svg.contains("fill=\"#1a1b26\""),145 "logo has no baked-in background (light theme must not show a dark slab)"146 );147 assert!(148 svg.contains("prefers-color-scheme:light"),149 "lettering flips ink on the light theme"150 );151 assert_sharp_no_gradient("logo.svg", &svg);152 // Logo-text (#27): hu13 lettering is hand-written <path>, never153 // <text>/fonts, so the header lockup has no font dependency.154 assert!(svg.contains("<path"), "logo letters are <path>");155 assert!(!svg.contains("<text"), "logo has no <text>");156 assert!(157 !svg.to_lowercase().contains("font"),158 "logo has no font refs"159 );160 assert!(161 svg.matches("<path").count() >= 2,162 "logo carries mark + letter paths"163 );164 }165 166 #[test]167 fn favicon_variant_is_simplified_mark_only() {168 // Logo-text (#27): the 16px favicon is the simplified mark-only169 // variant (no letters) so it stays legible; full lettering lives in170 // logo.svg. Both stay valid XML, sharp, tiny, with <2KB combined.171 let fav_path = manifest().join("static").join("favicon.svg");172 let fav_len = std::fs::metadata(&fav_path)173 .expect("static/favicon.svg exists")174 .len();175 assert!(176 fav_len < 2048,177 "favicon.svg is {fav_len} bytes, budget is 2KB"178 );179 let fav = read_static("favicon.svg");180 check_well_formed(&fav);181 assert!(fav.contains("<svg"), "favicon has an <svg> root");182 assert!(fav.contains("viewBox"), "favicon scales via viewBox");183 assert!(!fav.contains("<text"), "favicon has no <text>");184 assert!(185 !fav.to_lowercase().contains("font"),186 "favicon has no font refs"187 );188 assert!(189 !fav.contains("<path"),190 "favicon is mark-only (no letter <path>s)"191 );192 assert_sharp_no_gradient("favicon.svg", &fav);193 for color in ["#7aa2f7", "#7dcfff"] {194 assert!(fav.contains(color), "favicon keeps {color}");195 }196 assert!(197 !fav.contains("fill=\"#1a1b26\""),198 "favicon has no baked-in background"199 );200 let logo_len = std::fs::metadata(manifest().join("static").join("logo.svg"))201 .expect("static/logo.svg exists")202 .len();203 assert!(204 logo_len + fav_len < 2048,205 "logo ({logo_len}B) + favicon ({fav_len}B) stay under 2KB total"206 );207 let logo = read_static("logo.svg");208 assert_ne!(logo, fav, "favicon is a simplified variant, not a copy");209 }210 211 #[test]212 fn wordmark_variant_exists_tiny() {213 let path = manifest().join("static").join("wordmark.svg");214 let len = std::fs::metadata(&path)215 .expect("static/wordmark.svg exists")216 .len();217 assert!(len < 2048, "wordmark.svg is {len} bytes, budget is 2KB");218 let svg = read_static("wordmark.svg");219 check_well_formed(&svg);220 assert!(svg.contains("hu13"), "wordmark names hu13");221 assert_sharp_no_gradient("wordmark.svg", &svg);222 }223 224 #[test]225 fn ascii_banner_limits_and_aesthetic() {226 // Owner block-art (#26): 12-line block glyphs reading "hu13".227 // Charset is space + block set (█ ▓ ▒ ░ ▄ ▀ ▌ ▐) plus the legacy228 // line-art chars (| - / _ +) for forward compat — still no letters,229 // no `<>&"` so `<pre>` stays verbatim via esc().230 let art = read_static("ascii.txt");231 assert!(!art.trim().is_empty(), "banner is non-empty");232 let lines: Vec<&str> = art.lines().collect();233 assert_eq!(lines.len(), 12, "owner banner is 12 lines");234 let mut saw_block = false;235 for line in &lines {236 assert!(line.chars().count() <= 60, "banner line too wide: {line:?}");237 for ch in line.chars() {238 assert!(!ch.is_ascii_alphabetic(), "no letters, got {ch:?}");239 assert!(240 !matches!(ch, '<' | '>' | '&' | '"'),241 "nothing to escape, got {ch:?}"242 );243 saw_block |= matches!(ch, '█' | '▓' | '▒' | '░' | '▄' | '▀' | '▌' | '▐');244 assert!(245 matches!(246 ch,247 ' ' | '█'248 | '▓'249 | '▒'250 | '░'251 | '▄'252 | '▀'253 | '▌'254 | '▐'255 | '|'256 | '-'257 | '/'258 | '_'259 | '+'260 ),261 "block set + legacy (| - / _ + space), got {ch:?}"262 );263 }264 }265 assert!(saw_block, "uses block glyphs");266 assert!(art.contains('█'), "block fill present");267 }268 269 #[test]270 fn ascii_renders_inside_pre_unescaped_except_esc() {271 // The banner has no `<>&"` so `esc()` must leave it byte-identical;272 // wrapping it in `<pre>` is therefore exact (unit tests in `src/logo.rs`273 // assert the same on the `ascii_pre()` helper itself).274 let art = read_static("ascii.txt");275 assert!(!art.contains(['<', '>', '&', '"']), "nothing to escape");276 let pre = format!("<div class=\"pre-scroll\"><pre>\n{art}</pre></div>");277 assert!(pre.contains("<pre>"), "inside <pre>");278 assert!(pre.contains(&art), "verbatim, unescaped-except-esc()");279 }280 281 /// Strip literal SGR codes (`[...m`, no ESC byte) the way the rubric282 /// prescribes: `sed 's/\[[0-9;]*m//g'`.283 fn strip_literal_sgr(s: &str) -> String {284 let bytes = s.as_bytes();285 let mut out = String::with_capacity(s.len());286 let mut i = 0;287 while i < bytes.len() {288 if bytes[i] == b'[' {289 let mut k = i + 1;290 while k < bytes.len() && (bytes[k].is_ascii_digit() || bytes[k] == b';') {291 k += 1;292 }293 if k < bytes.len() && bytes[k] == b'm' && k > i + 1 {294 i = k + 1;295 continue;296 }297 }298 let ch = s[i..].chars().next().expect("char boundary");299 out.push(ch);300 i += ch.len_utf8();301 }302 out303 }304 305 #[test]306 fn ansi_strips_to_ascii_byte_for_byte() {307 // Owner ANSI original (#26) strips to the plain banner byte-for-byte.308 let ansi = read_static("logo-ansi.txt");309 let ascii = read_static("ascii.txt");310 assert_eq!(strip_literal_sgr(&ansi), ascii, "strip(ansi) == ascii");311 assert_eq!(ansi.lines().count(), 12, "ansi is 12 lines");312 assert_eq!(ascii.lines().count(), 12, "ascii is 12 lines");313 }314 315 #[test]316 fn logo_ansi_is_esc_free_with_codes() {317 // Stored with literal `[...m` codes, block glyphs, NO raw ESC bytes.318 let ansi = read_static("logo-ansi.txt");319 assert!(!ansi.contains('\u{1b}'), "no raw ESC bytes stored");320 assert!(ansi.contains("[0;"), "holds SGR codes");321 assert!(ansi.contains("[0m"), "holds reset");322 assert!(ansi.contains('█'), "holds block glyphs");323 }324 325 #[test]326 fn static_holds_no_esc_bytes() {327 // Success rubric (#26): `grep -r $'\\x1b' static/` is empty.328 let dir = manifest().join("static");329 for name in ["logo-ansi.txt", "ascii.txt", "logo.svg", "favicon.svg"] {330 let bytes =331 std::fs::read(dir.join(name)).unwrap_or_else(|_| panic!("static/{name} exists"));332 assert!(!bytes.contains(&0x1b), "static/{name} holds no ESC bytes");333 }334 }