#16 m6/logo
Patch
src/logo.rs+82 −0--- /dev/null+++ b/src/logo.rs@@ -0,0 +1,82 @@1+//! hu13 logo + ASCII banner (m6, f0rge issue #15).2+//!3+//! The mark lives hand-written in `static/logo.svg` (square icon, reads at4+//! 16px) plus `static/wordmark.svg` (horizontal lockup for the navbar); the5+//! text banner lives in `static/ascii.txt`.6+//!7+//! [`ASCII_ART`], [`LOGO_SVG`] and [`WORDMARK_SVG`] re-expose those static8+//! files for later issues (navbar #16, favicon #17, wiki README #19, agents9+//! page) with no build step.10+//!11+//! [`ascii_pre`] renders the banner as an escaped `<pre>` block (see12+//! [`crate::layout::esc`]): the art itself is plain `|/-_` plus spaces so13+//! escaping is a no-op today, but the wrapper keeps the invariant that any14+//! future art containing `<>&"` stays inert inside HTML.15+16+use crate::layout::esc;17+18+// TODO(m6+): stubs only — navbar (#16), favicon (#17), wiki README (#19)19+// and the agents page will call these; allow until wired.20+#[allow(dead_code)]21+/// ASCII banner, byte-for-byte `static/ascii.txt` (6 lines (5 art + tagline), <=60 cols).22+pub const ASCII_ART: &str = include_str!("../static/ascii.txt");23+24+#[allow(dead_code)]25+/// Square mark, byte-for-byte `static/logo.svg`.26+pub const LOGO_SVG: &str = include_str!("../static/logo.svg");27+28+#[allow(dead_code)]29+/// Horizontal lockup, byte-for-byte `static/wordmark.svg`.30+pub const WORDMARK_SVG: &str = include_str!("../static/wordmark.svg");31+32+/// Render the ASCII banner as an escaped `<pre>` block.33+///34+/// `pre-scroll` matches the scroll-container convention in35+/// `static/style.css` so long lines never break the 360px layout.36+#[allow(dead_code)]37+#[must_use]38+pub fn ascii_pre() -> String {39+ format!(40+ "<div class=\"pre-scroll\"><pre class=\"logo-ascii\">\n{}</pre></div>\n",41+ esc(ASCII_ART)42+ )43+}44+45+#[cfg(test)]46+mod tests {47+ use super::*;48+49+ #[test]50+ fn ascii_const_is_small_banner() {51+ assert!(!ASCII_ART.is_empty(), "banner is non-empty");52+ let lines: Vec<&str> = ASCII_ART.lines().collect();53+ assert!(54+ (2..=12).contains(&lines.len()),55+ "2-12 lines, got {}",56+ lines.len()57+ );58+ for line in &lines {59+ assert!(line.chars().count() <= 60, "line too wide: {line:?}");60+ }61+ }62+63+ #[test]64+ fn ascii_pre_wraps_escaped_pre() {65+ // The art is escapable-invariant: esc() is a no-op on it.66+ assert_eq!(esc(ASCII_ART), ASCII_ART, "art needs no escaping");67+ let pre = ascii_pre();68+ assert!(pre.contains("<pre"), "renders inside <pre>");69+ assert!(pre.contains("pre-scroll"), "scroll wrapper kept");70+ assert!(pre.contains(ASCII_ART), "banner inlined verbatim");71+ assert!(pre.contains(&esc(ASCII_ART)), "inlined via esc()");72+ assert!(!pre.contains("<script"), "no script smuggling");73+ }74+75+ #[test]76+ fn svg_consts_match_tiny_marks() {77+ for (name, svg) in [("logo", LOGO_SVG), ("wordmark", WORDMARK_SVG)] {78+ assert!(svg.contains("<svg"), "{name} const holds svg");79+ assert!(svg.len() < 2048, "{name} const stays tiny");80+ }81+ }82+}
src/main.rs+1 −0--- a/src/main.rs+++ b/src/main.rs@@ -7,6 +7,7 @@ mod forge;77 mod gitx;88 mod inbox;99 mod layout;10+mod logo;1011 mod releases;1112 mod serve;1213 mod wiki;
static/ascii.txt+6 −0--- /dev/null+++ b/static/ascii.txt@@ -0,0 +1,6 @@1+| | ___2+|___ | |3+| | | | | ___|4+| | | | | |5+| | |___| | |___|6+hu13 - one repo, one hub
static/logo.svg+8 −0--- /dev/null+++ b/static/logo.svg@@ -0,0 +1,8 @@1+<?xml version="1.0" encoding="UTF-8"?>2+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="hu13 logo">3+ <title>hu13</title>4+ <rect x="0" y="0" width="32" height="32" fill="#1a1b26"/>5+ <rect x="0.5" y="0.5" width="31" height="31" fill="none" stroke="#292e42"/>6+ <polygon points="16,6 26,16 16,26 6,16" fill="none" stroke="#7aa2f7" stroke-width="3"/>7+ <rect x="13" y="13" width="6" height="6" fill="#7dcfff"/>8+</svg>
static/wordmark.svg+8 −0--- /dev/null+++ b/static/wordmark.svg@@ -0,0 +1,8 @@1+<?xml version="1.0" encoding="UTF-8"?>2+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 32" width="128" height="32" role="img" aria-label="hu13 wordmark">3+ <title>hu13</title>4+ <rect x="0" y="0" width="128" height="32" fill="#1a1b26"/>5+ <polygon points="16,4 28,16 16,28 4,16" fill="none" stroke="#7aa2f7" stroke-width="3"/>6+ <rect x="13" y="13" width="6" height="6" fill="#7dcfff"/>7+ <text x="36" y="22" font-family="monospace" font-size="16" font-weight="bold" fill="#c0caf5">hu13</text>8+</svg>
tests/logo.rs+198 −1--- /dev/null+++ b/tests/logo.rs@@ -0,0 +1,198 @@1+//! Logo suite (m6, f0rge issue #15):2+//! `static/logo.svg` exists + valid XML + <2KB, wordmark variant,3+//! ASCII banner limits, `<pre>` rendering invariant.4+//!5+//! File-level suite (hu13 is a binary crate, so `src/logo.rs` unit tests6+//! cover the `ascii_pre()` helper itself; here we assert on disk).7+8+use std::path::PathBuf;9+10+fn manifest() -> PathBuf {11+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))12+}13+14+fn read_static(name: &str) -> String {15+ let path = manifest().join("static").join(name);16+ std::fs::read_to_string(&path).unwrap_or_else(|_| panic!("static/{name} exists"))17+}18+19+/// Minimal well-formedness check: balanced tags, quoted attributes, single20+/// root; skips comments, PIs and `<!...>` declarations. Enough to call the21+/// hand-written mark "valid XML" without a new dependency.22+fn check_well_formed(xml: &str) {23+ let bytes = xml.as_bytes();24+ let mut i = 0;25+ let mut stack: Vec<String> = Vec::new();26+ let mut root_done = false;27+ while i < bytes.len() {28+ if bytes[i] != b'<' {29+ i += 1;30+ continue;31+ }32+ let rest = &xml[i..];33+ if rest.starts_with("<!--") {34+ let end = rest.find("-->").expect("unterminated comment");35+ i += end + 4;36+ continue;37+ }38+ if rest.starts_with("<?") {39+ let end = rest.find("?>").expect("unterminated PI");40+ i += end + 2;41+ continue;42+ }43+ if rest.starts_with("<!") {44+ let end = rest.find('>').expect("unterminated declaration");45+ i += end + 1;46+ continue;47+ }48+ // Tag end, respecting quoted `>` inside attributes.49+ let mut j = i + 1;50+ let mut quote: Option<u8> = None;51+ while j < bytes.len() {52+ match (quote, bytes[j]) {53+ (Some(q), c) if c == q => quote = None,54+ (None, b'"' | b'\'') => quote = Some(bytes[j]),55+ (None, b'>') => break,56+ _ => {}57+ }58+ j += 1;59+ }60+ assert!(j < bytes.len(), "unterminated tag near {rest:?}");61+ let tag = &xml[i..=j];62+ if let Some(close) = tag.strip_prefix("</") {63+ let name = close.trim_end_matches('>').trim().to_string();64+ let open = stack.pop().expect("closing tag without opener");65+ assert_eq!(open, name, "mismatched tag: <{open}> vs </{name}>");66+ if stack.is_empty() {67+ root_done = true;68+ }69+ } else {70+ let self_close = tag.ends_with("/>");71+ let mut inner = &tag[1..tag.len() - 1];72+ inner = inner.strip_suffix('/').unwrap_or(inner);73+ let name: &str = inner74+ .split(|c: char| c.is_whitespace() || c == '/')75+ .next()76+ .unwrap_or("");77+ assert!(!name.is_empty(), "empty tag name in {tag:?}");78+ assert!(79+ name.bytes()80+ .next()81+ .is_some_and(|c| c.is_ascii_alphabetic() || c == b'_' || c == b':'),82+ "bad tag name {name:?}"83+ );84+ if self_close {85+ if stack.is_empty() {86+ root_done = true;87+ }88+ } else {89+ assert!(!root_done, "second root element <{name}>");90+ stack.push(name.to_string());91+ }92+ }93+ i = j + 1;94+ }95+ assert!(stack.is_empty(), "unclosed tags: {stack:?}");96+ assert!(root_done, "no root element");97+}98+99+fn assert_sharp_no_gradient(name: &str, svg: &str) {100+ let lower = svg.to_lowercase();101+ for bad in [102+ "rx=",103+ "ry=",104+ "<circle",105+ "<ellipse",106+ "border-radius",107+ "linecap=\"round\"",108+ "linejoin=\"round\"",109+ ] {110+ assert!(111+ !lower.contains(bad),112+ "{name}: no rounded corners ({bad}), sharp geometry only"113+ );114+ }115+ assert!(116+ !lower.contains("gradient"),117+ "{name}: no gradients (must stay legible at 16px)"118+ );119+}120+121+#[test]122+fn logo_svg_exists_valid_tiny() {123+ let path = manifest().join("static").join("logo.svg");124+ let len = std::fs::metadata(&path)125+ .expect("static/logo.svg exists")126+ .len();127+ assert!(len < 2048, "logo.svg is {len} bytes, budget is 2KB");128+ assert!(len > 100, "logo.svg is {len} bytes, suspiciously small");129+ let svg = read_static("logo.svg");130+ check_well_formed(&svg);131+ assert!(svg.contains("<svg"), "has an <svg> root");132+ assert!(133+ svg.contains("http://www.w3.org/2000/svg"),134+ "svg namespace present"135+ );136+ assert!(svg.contains("viewBox"), "scales via viewBox (16px + 32px)");137+ for color in ["#1a1b26", "#7aa2f7", "#7dcfff"] {138+ assert!(svg.contains(color), "Tokyo Night palette keeps {color}");139+ }140+ assert_sharp_no_gradient("logo.svg", &svg);141+}142+143+#[test]144+fn wordmark_variant_exists_tiny() {145+ let path = manifest().join("static").join("wordmark.svg");146+ let len = std::fs::metadata(&path)147+ .expect("static/wordmark.svg exists")148+ .len();149+ assert!(len < 2048, "wordmark.svg is {len} bytes, budget is 2KB");150+ let svg = read_static("wordmark.svg");151+ check_well_formed(&svg);152+ assert!(svg.contains("hu13"), "wordmark names hu13");153+ assert_sharp_no_gradient("wordmark.svg", &svg);154+}155+156+#[test]157+fn ascii_banner_limits_and_aesthetic() {158+ let art = read_static("ascii.txt");159+ assert!(!art.trim().is_empty(), "banner is non-empty");160+ let lines: Vec<&str> = art.lines().collect();161+ assert!(162+ lines.len() <= 12,163+ "banner is {} lines, cap is 12",164+ lines.len()165+ );166+ assert!(lines.len() >= 2, "banner is a banner, not one line");167+ let mut saw_pipe = false;168+ let mut saw_hyphen = false;169+ let mut saw_slash = false;170+ for line in &lines {171+ assert!(line.chars().count() <= 60, "banner line too wide: {line:?}");172+ for ch in line.chars() {173+ saw_pipe |= ch == '|';174+ saw_hyphen |= ch == '-';175+ saw_slash |= ch == '/';176+ assert!(177+ matches!(ch, '|' | '-' | '/' | '_' | '+' | ' '),178+ "sharp-corner aesthetic only (| - / _ + space), got {ch:?}"179+ );180+ }181+ }182+ assert!(183+ saw_pipe && saw_hyphen && saw_slash,184+ "uses pipes+hyphens+slashes"185+ );186+}187+188+#[test]189+fn ascii_renders_inside_pre_unescaped_except_esc() {190+ // The banner has no `<>&"` so `esc()` must leave it byte-identical;191+ // wrapping it in `<pre>` is therefore exact (unit tests in `src/logo.rs`192+ // assert the same on the `ascii_pre()` helper itself).193+ let art = read_static("ascii.txt");194+ assert!(!art.contains(['<', '>', '&', '"']), "nothing to escape");195+ let pre = format!("<div class=\"pre-scroll\"><pre>\n{art}</pre></div>");196+ assert!(pre.contains("<pre>"), "inside <pre>");197+ assert!(pre.contains(&art), "verbatim, unescaped-except-esc()");198+}0--
Comments (0)
No comments yet.
Reviews (0)
No reviews yet.