use std::fmt::Write;
pub fn num_into(buf: &mut String, v: f64) {
if v.is_finite() {
let rounded = (v * 1000.0).round() / 1000.0;
if rounded == rounded.trunc() {
let _ = write!(buf, "{}", rounded as i64);
} else {
let s = format!("{:.3}", rounded);
let trimmed = s.trim_end_matches('0').trim_end_matches('.');
buf.push_str(trimmed);
}
} else {
buf.push('0');
}
}
pub fn num(v: f64) -> String {
let mut s = String::new();
num_into(&mut s, v);
s
}
pub fn escape_into(buf: &mut String, s: &str) {
for c in s.chars() {
match c {
'&' => buf.push_str("&"),
'<' => buf.push_str("<"),
'>' => buf.push_str(">"),
'"' => buf.push_str("""),
'\'' => buf.push_str("'"),
other => buf.push(other),
}
}
}
pub fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
escape_into(&mut out, s);
out
}
pub fn document_open(buf: &mut String, width: f64, height: f64) {
buf.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
buf.push_str("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ");
num_into(buf, width);
buf.push(' ');
num_into(buf, height);
buf.push_str("\" width=\"");
num_into(buf, width);
buf.push_str("\" height=\"");
num_into(buf, height);
buf.push_str("\" shape-rendering=\"geometricPrecision\" font-family=\"Times, 'Liberation Serif', serif\">");
}
pub fn document_close(buf: &mut String) {
buf.push_str("</svg>");
}
pub fn rect(buf: &mut String, x: f64, y: f64, w: f64, h: f64, extra: &str) {
buf.push_str("<rect x=\"");
num_into(buf, x);
buf.push_str("\" y=\"");
num_into(buf, y);
buf.push_str("\" width=\"");
num_into(buf, w);
buf.push_str("\" height=\"");
num_into(buf, h);
buf.push('"');
buf.push_str(extra);
buf.push_str("/>");
}
pub fn line(buf: &mut String, x1: f64, y1: f64, x2: f64, y2: f64, extra: &str) {
buf.push_str("<line x1=\"");
num_into(buf, x1);
buf.push_str("\" y1=\"");
num_into(buf, y1);
buf.push_str("\" x2=\"");
num_into(buf, x2);
buf.push_str("\" y2=\"");
num_into(buf, y2);
buf.push('"');
buf.push_str(extra);
buf.push_str("/>");
}
pub fn circle(buf: &mut String, cx: f64, cy: f64, r: f64, extra: &str) {
buf.push_str("<circle cx=\"");
num_into(buf, cx);
buf.push_str("\" cy=\"");
num_into(buf, cy);
buf.push_str("\" r=\"");
num_into(buf, r);
buf.push('"');
buf.push_str(extra);
buf.push_str("/>");
}
pub fn polyline(buf: &mut String, points: &[(f64, f64)], extra: &str) {
buf.push_str("<polyline points=\"");
write_points(buf, points);
buf.push_str("\" fill=\"none\"");
buf.push_str(extra);
buf.push_str("/>");
}
pub fn polygon(buf: &mut String, points: &[(f64, f64)], extra: &str) {
buf.push_str("<polygon points=\"");
write_points(buf, points);
buf.push('"');
buf.push_str(extra);
buf.push_str("/>");
}
fn write_points(buf: &mut String, points: &[(f64, f64)]) {
for (i, (x, y)) in points.iter().enumerate() {
if i > 0 {
buf.push(' ');
}
num_into(buf, *x);
buf.push(',');
num_into(buf, *y);
}
}
pub fn text(buf: &mut String, x: f64, y: f64, content: &str, extra: &str) {
buf.push_str("<text x=\"");
num_into(buf, x);
buf.push_str("\" y=\"");
num_into(buf, y);
buf.push('"');
buf.push_str(extra);
buf.push('>');
buf.push_str(content);
buf.push_str("</text>");
}
pub fn group_open(buf: &mut String, extra: &str) {
buf.push_str("<g");
buf.push_str(extra);
buf.push('>');
}
pub fn group_close(buf: &mut String) {
buf.push_str("</g>");
}
pub struct Attrs {
pub buf: String,
}
impl Attrs {
pub fn new() -> Self {
Self { buf: String::new() }
}
pub fn str(mut self, k: &str, v: &str) -> Self {
self.buf.push(' ');
self.buf.push_str(k);
self.buf.push_str("=\"");
escape_into(&mut self.buf, v);
self.buf.push('"');
self
}
pub fn num(mut self, k: &str, v: f64) -> Self {
self.buf.push(' ');
self.buf.push_str(k);
self.buf.push_str("=\"");
num_into(&mut self.buf, v);
self.buf.push('"');
self
}
pub fn opt_str(self, k: &str, v: Option<&str>) -> Self {
match v {
Some(s) => self.str(k, s),
None => self,
}
}
pub fn into_string(self) -> String {
self.buf
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn integer_floats_render_without_decimals() {
assert_eq!(num(3.0), "3");
assert_eq!(num(0.0), "0");
assert_eq!(num(-7.0), "-7");
}
#[test]
fn fractional_values_trim_trailing_zeros() {
assert_eq!(num(1.5), "1.5");
assert_eq!(num(1.250), "1.25");
assert_eq!(num(1.234), "1.234");
}
#[test]
fn escape_handles_all_xml_specials() {
assert_eq!(escape("a&b<c>d\"e'f"), "a&b<c>d"e'f");
}
}