1 //! hu13 logo + ASCII banner (m6, f0rge issues #15 + #26 + #27 + #28).2 //!3 //! The lockup lives hand-written in `static/logo.svg` (diamond-hub mark +4 //! hu13 lettered as <path>, no <text>/fonts; reads at 24px header size)5 //! plus `static/wordmark.svg` (legacy horizontal lockup); the simplified6 //! 16px favicon variant lives in `static/favicon.svg` (mark only, no7 //! letters — small sizes stay legible, full lettering stays in logo.svg);8 //! the text banner lives in `static/ascii.txt` (owner block-art, 12 lines,9 //! stripped from `static/logo-ansi.txt`).10 //!11 //! [`ASCII_ART`], [`LOGO_ANSI_RAW`], [`LOGO_SVG`], [`GL0W_SVG`],12 //! [`FAVICON_SVG`] and [`WORDMARK_SVG`] re-expose those static files for13 //! later issues (navbar #16, favicon #17, wiki README #19, agents page)14 //! with no build step.15 //!16 //! [`ascii_pre`] renders the banner as an escaped `<pre>` block (see17 //! [`crate::layout::esc`]): the art itself is block glyphs plus spaces so18 //! escaping is a no-op today, but the wrapper keeps the invariant that any19 //! future art containing `<>&"` stays inert inside HTML.20 //!21 //! `static/logo-ansi.txt` stores the owner's 12-line ANSI art with literal22 //! `[...m` codes and NO raw ESC bytes (repo stays ESC-free); terminal use23 //! goes through [`logo_ansi_for_tty`], which prefixes each SGR code with24 //! ESC at runtime so a tty renders colors while pipes stay plain.25 26 use crate::layout::esc;27 28 // TODO(m6+): stubs only — navbar (#16), favicon (#17), wiki README (#19)29 // and the agents page will call these; allow until wired.30 #[allow(dead_code)]31 /// ASCII banner, byte-for-byte `static/ascii.txt` (owner block-art,32 /// 12 lines, 42 cols, block glyphs + spaces).33 pub const ASCII_ART: &str = include_str!("../static/ascii.txt");34 35 #[allow(dead_code)]36 /// Owner ANSI block-art, byte-for-byte `static/logo-ansi.txt` (12 lines).37 ///38 /// Stored with literal `[...m` SGR codes and NO raw ESC bytes (see raw39 /// file); use [`logo_ansi_for_tty`] for terminal output and40 /// [`strip_ansi_sgr`] for the plain render.41 pub const LOGO_ANSI_RAW: &str = include_str!("../static/logo-ansi.txt");42 43 #[allow(dead_code)]44 /// Header lockup, byte-for-byte `static/logo.svg` (mark + hu13 as <path>).45 pub const LOGO_SVG: &str = include_str!("../static/logo.svg");46 47 /// gl0w mark, byte-for-byte `static/gl0w.svg` (from the gf1 brand source).48 /// Inlined into the header/footer so `currentColor` follows the site theme.49 pub const GL0W_SVG: &str = include_str!("../static/gl0w.svg");50 51 #[allow(dead_code)]52 /// Simplified 16px favicon variant, byte-for-byte `static/favicon.svg`53 /// (mark only, no letters — documented choice for small-size legibility).54 pub const FAVICON_SVG: &str = include_str!("../static/favicon.svg");55 56 #[allow(dead_code)]57 /// Horizontal lockup, byte-for-byte `static/wordmark.svg`.58 pub const WORDMARK_SVG: &str = include_str!("../static/wordmark.svg");59 60 /// Render the ASCII banner as an escaped `<pre>` block.61 ///62 /// `pre-scroll` matches the scroll-container convention in63 /// `static/style.css` so long lines never break the 360px layout.64 #[allow(dead_code)]65 #[must_use]66 pub fn ascii_pre() -> String {67 format!(68 "<div class=\"pre-scroll\"><pre class=\"logo-ascii\">\n{}</pre></div>\n",69 esc(ASCII_ART)70 )71 }72 73 /// Strip SGR color codes from owner ANSI art.74 ///75 /// Removes literal `[...m` sequences (`[` + digits/`;` + `m`) plus the same76 /// sequences with a leading ESC byte (`\x1b[...m`, as produced by77 /// [`interpret_ansi_sgr`]), leaving the plain block glyphs. Equivalent to78 /// `sed 's/\[[0-9;]*m//g'` on the ESC-free stored file.79 #[allow(dead_code)]80 #[must_use]81 pub fn strip_ansi_sgr(s: &str) -> String {82 let bytes = s.as_bytes();83 let mut out = String::with_capacity(s.len());84 let mut i = 0;85 while i < bytes.len() {86 let mut j = i;87 // Optional single ESC prefix (interpreted form).88 if bytes[j] == 0x1b && j + 1 < bytes.len() && bytes[j + 1] == b'[' {89 j += 1;90 }91 if bytes[j] == b'[' {92 let mut k = j + 1;93 while k < bytes.len() && (bytes[k].is_ascii_digit() || bytes[k] == b';') {94 k += 1;95 }96 if k < bytes.len() && bytes[k] == b'm' && k > j + 1 {97 i = k + 1;98 continue;99 }100 }101 let ch = s[i..].chars().next().expect("char boundary");102 out.push(ch);103 i += ch.len_utf8();104 }105 out106 }107 108 /// Prefix each literal SGR code (`[...m`) with ESC for tty output.109 ///110 /// The stored file is ESC-free so the repo never holds raw ESC bytes;111 /// this converts at runtime so a terminal renders magenta/cyan/grey.112 /// A `[` not followed by digits/`;` + `m` is left alone.113 #[allow(dead_code)]114 #[must_use]115 pub fn interpret_ansi_sgr(s: &str) -> String {116 let bytes = s.as_bytes();117 let mut out = String::with_capacity(s.len() + 64);118 let mut i = 0;119 while i < bytes.len() {120 if bytes[i] == b'[' {121 let mut k = i + 1;122 while k < bytes.len() && (bytes[k].is_ascii_digit() || bytes[k] == b';') {123 k += 1;124 }125 if k < bytes.len() && bytes[k] == b'm' && k > i + 1 {126 out.push('\u{1b}');127 let ch = s[i..].chars().next().expect("char boundary");128 out.push(ch);129 i += ch.len_utf8();130 continue;131 }132 }133 let ch = s[i..].chars().next().expect("char boundary");134 out.push(ch);135 i += ch.len_utf8();136 }137 out138 }139 140 /// Owner ANSI art interpreted for a tty (ESC-prefixed SGR codes).141 #[allow(dead_code)]142 #[must_use]143 pub fn logo_ansi_for_tty() -> String {144 interpret_ansi_sgr(LOGO_ANSI_RAW)145 }146 147 #[cfg(test)]148 mod tests {149 use super::*;150 151 #[test]152 fn ascii_const_is_small_banner() {153 assert!(!ASCII_ART.is_empty(), "banner is non-empty");154 let lines: Vec<&str> = ASCII_ART.lines().collect();155 assert!(156 (2..=12).contains(&lines.len()),157 "2-12 lines, got {}",158 lines.len()159 );160 for line in &lines {161 assert!(line.chars().count() <= 60, "line too wide: {line:?}");162 }163 }164 165 #[test]166 fn ascii_pre_wraps_escaped_pre() {167 // The art is escapable-invariant: esc() is a no-op on it.168 assert_eq!(esc(ASCII_ART), ASCII_ART, "art needs no escaping");169 let pre = ascii_pre();170 assert!(pre.contains("<pre"), "renders inside <pre>");171 assert!(pre.contains("pre-scroll"), "scroll wrapper kept");172 assert!(pre.contains(ASCII_ART), "banner inlined verbatim");173 assert!(pre.contains(&esc(ASCII_ART)), "inlined via esc()");174 assert!(!pre.contains("<script"), "no script smuggling");175 }176 177 #[test]178 fn ansi_raw_is_esc_free_with_codes() {179 // Owner file (#26): literal `[...m` codes, block glyphs, no ESC.180 assert!(!LOGO_ANSI_RAW.contains('\u{1b}'), "no raw ESC bytes stored");181 assert!(LOGO_ANSI_RAW.contains("[0;"), "holds SGR codes");182 assert!(LOGO_ANSI_RAW.contains('█'), "holds block glyphs");183 assert_eq!(LOGO_ANSI_RAW.lines().count(), 12, "12 lines");184 }185 186 #[test]187 fn strip_yields_ascii() {188 assert_eq!(strip_ansi_sgr(LOGO_ANSI_RAW), ASCII_ART, "strip == ascii");189 assert_eq!(strip_ansi_sgr(ASCII_ART), ASCII_ART, "plain is stable");190 }191 192 #[test]193 fn tty_interpretation_holds_esc_and_strips_back() {194 let tty = logo_ansi_for_tty();195 assert!(tty.contains('\u{1b}'), "tty form carries ESC");196 assert!(tty.contains('█'), "glyphs kept");197 assert_eq!(strip_ansi_sgr(&tty), ASCII_ART, "tty strips to ascii");198 }199 200 #[test]201 fn svg_consts_match_tiny_marks() {202 for (name, svg) in [203 ("logo", LOGO_SVG),204 ("favicon", FAVICON_SVG),205 ("wordmark", WORDMARK_SVG),206 ] {207 assert!(svg.contains("<svg"), "{name} const holds svg");208 assert!(svg.len() < 2048, "{name} const stays tiny");209 }210 // Logo-text (#27): lockup letters are <path>, never <text>/fonts.211 assert!(LOGO_SVG.contains("<path"), "logo letters are paths");212 assert!(!LOGO_SVG.contains("<text"), "logo has no <text>");213 assert!(214 !LOGO_SVG.to_lowercase().contains("font"),215 "logo has no font refs"216 );217 // Simplified favicon variant (#27): mark only, no letters.218 assert!(!FAVICON_SVG.contains("<text"), "favicon has no <text>");219 assert!(220 !FAVICON_SVG.contains("<path"),221 "favicon is mark-only (no letter paths)"222 );223 }224 }