src/logo.rs 8.4 KiB 224 lines Raw
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 }