1.5 KiB · text History 487bb12
//! Small SVG string helpers shared by the card and the decorations.

/// Escape text for SVG character data and attribute values. Characters XML
/// 1.0 forbids outright (most C0 controls) are dropped, since a single one
/// would make the whole document unparseable.
pub fn esc(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for ch in s.chars() {
        match ch {
            '&' => out.push_str("&"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&#39;"),
            '\t' | '\n' | '\r' => out.push(ch),
            c if (c as u32) < 0x20 || c == '\u{fffe}' || c == '\u{ffff}' => {}
            c => out.push(c),
        }
    }
    out
}

/// Path data for a rectangle with independent top and bottom corner radii.
pub fn round_path(x: f32, y: f32, w: f32, h: f32, rt: f32, rb: f32) -> String {
    let rt = rt.min(w / 2.0).min(h / 2.0).max(0.0);
    let rb = rb.min(w / 2.0).min(h / 2.0).max(0.0);
    let (x1, y1) = (x + w, y + h);
    format!(
        "M{} {y}H{}A{rt} {rt} 0 0 1 {x1} {}V{}A{rb} {rb} 0 0 1 {} {y1}H{}A{rb} {rb} 0 0 1 {x} {}V{}A{rt} {rt} 0 0 1 {} {y}Z",
        x + rt,
        x1 - rt,
        y + rt,
        y1 - rb,
        x1 - rb,
        x + rb,
        y1 - rb,
        y + rt,
        x + rt,
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn esc_drops_xml_illegal_controls() {
        assert_eq!(esc("a\u{0}b<&>\u{1b}"), "ab&lt;&amp;&gt;");
    }
}