1use super::backend::{
8 DrawBackend, FontFace, LineEnd, LineStyle, PointShape, PointStyle, RectStyle, TextAnchor,
9 TextStyle,
10};
11use super::{Rect, RenderError};
12
13pub struct SvgBackend {
20 plot_area: Rect,
21 total_area: Rect,
22 body: String,
23 tooltip: Option<String>,
24 axis_key: Option<String>,
25 series_key: Option<String>,
26 value_key: Option<String>,
27 mark_part: Option<String>,
28 mark_attrs: Vec<(String, String)>,
29 mark_geom: Option<String>,
31 root_attrs: Vec<(String, String)>,
32 warnings: Vec<String>,
33 plot_attr: bool,
36 formatted_values: bool,
39 mark_fvalue: Option<String>,
42 mark_fx: Option<String>,
45}
46
47impl SvgBackend {
48 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
49 SvgBackend {
50 plot_area,
51 total_area: Rect {
52 x: 0.0,
53 y: 0.0,
54 width: width as f64,
55 height: height as f64,
56 },
57 body: String::new(),
58 tooltip: None,
59 axis_key: None,
60 series_key: None,
61 value_key: None,
62 mark_part: None,
63 mark_attrs: Vec::new(),
64 mark_geom: None,
65 root_attrs: Vec::new(),
66 warnings: Vec::new(),
67 plot_attr: true,
68 formatted_values: false,
69 mark_fvalue: None,
70 mark_fx: None,
71 }
72 }
73
74 pub(crate) fn set_formatted_values(&mut self, on: bool) {
78 self.formatted_values = on;
79 }
80
81 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
85 self.root_attrs = attrs;
86 }
87
88 pub fn extend_root_attrs(&mut self, attrs: Vec<(String, String)>) {
96 self.root_attrs.extend(attrs);
97 }
98
99 pub(crate) fn without_plot_attr(&mut self) {
101 self.plot_attr = false;
102 }
103
104 pub(crate) fn push_raw(&mut self, markup: &str) {
107 self.body.push_str(markup);
108 }
109
110 pub(crate) fn body(&self) -> &str {
112 &self.body
113 }
114
115 pub fn take_warnings(&mut self) -> Vec<String> {
117 std::mem::take(&mut self.warnings)
118 }
119
120 fn mark_class(&self, kind: &str) -> String {
123 match &self.mark_geom {
124 Some(g) => format!(" class=\"gg-mark gg-{kind}\" data-geom=\"{}\"", escape(g)),
125 None => String::new(),
126 }
127 }
128
129 fn push_mark(&mut self, tag: &str, kind: &str, attrs: &str) {
137 let mut pairs: Vec<(String, String)> = Vec::new();
147 for (name, val) in [
148 ("data-x", &self.axis_key),
149 ("data-series", &self.series_key),
150 ("data-value", &self.value_key),
151 ] {
152 if let Some(v) = val {
153 pairs.push((name.to_string(), v.clone()));
154 }
155 }
156 if let Some(fx) = &self.mark_fx {
160 if self.axis_key.as_deref() != Some(fx.as_str()) {
161 pairs.push(("data-fx".to_string(), fx.clone()));
162 }
163 }
164 if let Some(fvalue) = &self.mark_fvalue {
165 pairs.push(("data-fvalue".to_string(), fvalue.clone()));
166 }
167 for (name, val) in &self.mark_attrs {
168 if !is_valid_mark_attr_name(name) {
169 continue;
170 }
171 match pairs.iter_mut().find(|(n, _)| n == name) {
172 Some(existing) => existing.1 = val.clone(),
173 None => pairs.push((name.clone(), val.clone())),
174 }
175 }
176 if let Some(part) = &self.mark_part {
181 match pairs.iter_mut().find(|(n, _)| n == "data-part") {
182 Some(existing) => existing.1 = part.clone(),
183 None => pairs.push(("data-part".to_string(), part.clone())),
184 }
185 }
186 let mut data = String::new();
187 for (name, val) in &pairs {
188 data.push_str(&format!(" {name}=\"{}\"", escape(val)));
189 }
190 if self.mark_part.is_some() {
191 data.push_str(" aria-hidden=\"true\"");
192 }
193 let effective_kind: &str = self.mark_part.as_deref().unwrap_or(kind);
198 data.push_str(&self.mark_class(effective_kind));
199 let mark = if self.mark_part.is_some() {
200 format!("<{tag} {attrs}{data}/>")
201 } else {
202 match &self.tooltip {
203 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
204 None => format!("<{tag} {attrs}{data}/>"),
205 }
206 };
207 self.body.push_str(&mark);
208 }
209
210 fn root_open(&self, prefix: &str) -> String {
211 let p = &self.plot_area;
212 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
213 let mut extra = String::new();
214 for (k, v) in &self.root_attrs {
215 extra.push_str(&format!(" {k}=\"{}\"", escape(v)));
216 }
217 let plot = if self.plot_attr {
218 format!(
219 " data-plot=\"{} {} {} {}\"",
220 num(p.x),
221 num(p.y),
222 num(p.width),
223 num(p.height),
224 )
225 } else {
226 String::new()
227 };
228 format!("<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{extra}>")
229 }
230
231 pub fn finish(self) -> String {
235 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
236 format!("{open}{}</svg>", self.body)
237 }
238
239 pub fn finish_fragment(self, x: f64, y: f64) -> String {
244 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
245 format!("{open}{}</svg>", self.body)
246 }
247
248 fn draw_line_run(&mut self, pts: &[(f64, f64)], style: &LineStyle) {
251 let dash = match style
252 .linetype
253 .pattern()
254 .iter()
255 .flat_map(|(d, g)| [*d, *g])
256 .map(|v| format!("{v}"))
257 .collect::<Vec<_>>()
258 .join(",")
259 {
260 s if s.is_empty() => String::new(),
261 s => format!(" stroke-dasharray=\"{s}\""),
262 };
263 let linecap = match style.lineend {
266 LineEnd::Butt => "",
267 LineEnd::Round => " stroke-linecap=\"round\"",
268 LineEnd::Square => " stroke-linecap=\"square\"",
269 };
270 let attrs = format!(
271 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
272 points(pts),
273 rgb(style.color),
274 finite_or(style.width, 1.0),
275 opacity_attr("stroke", style.alpha),
276 dash,
277 linecap
278 );
279 self.push_mark("polyline", "line", &attrs);
280 }
281}
282
283fn num(v: f64) -> String {
286 if v.is_finite() {
287 format!("{v:.2}")
288 } else {
289 "0".to_string()
290 }
291}
292
293fn finite_or(v: f64, fallback: f64) -> f64 {
294 if v.is_finite() {
295 v
296 } else {
297 fallback
298 }
299}
300
301fn opacity_attr(prefix: &str, alpha: f64) -> String {
307 let a = finite_or(alpha, 1.0);
308 if format!("{a:.3}") == "1.000" {
309 String::new()
310 } else {
311 format!(" {prefix}-opacity=\"{a:.3}\"")
312 }
313}
314
315pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
332 use crate::aes::Aesthetic;
333 let mut out = Vec::new();
334 if !built.panel_scales.is_empty() {
335 return out;
336 }
337 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
338 match built.scales.get(aes) {
339 Some(s) if s.is_discrete() => {
340 let labels: Vec<String> = s
341 .breaks()
342 .into_iter()
343 .map(|(_, l)| json_string(&l))
344 .collect();
345 (None, Some(format!("[{}]", labels.join(","))))
346 }
347 Some(s) => (s.expanded_domain(), None),
348 None => (None, None),
349 }
350 };
351 let (xd, xl) = axis(&Aesthetic::X);
352 let (yd, yl) = axis(&Aesthetic::Y);
353 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
354 out.push((
355 "data-domain".into(),
356 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
357 ));
358 }
359 if let Some((a, b)) = xd {
360 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
361 }
362 if let Some((a, b)) = yd {
363 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
364 }
365 if let Some(l) = xl {
366 out.push(("data-xlevels".into(), l));
367 }
368 if let Some(l) = yl {
369 out.push(("data-ylevels".into(), l));
370 }
371 if built.coord.is_flipped() {
372 out.push(("data-flip".into(), "true".into()));
373 }
374 out
375}
376
377fn g(v: f64) -> String {
380 format!("{v}")
381}
382
383fn json_string(s: &str) -> String {
386 let mut out = String::with_capacity(s.len() + 2);
387 out.push('"');
388 for c in s.chars() {
389 match c {
390 '"' => out.push_str("\\\""),
391 '\\' => out.push_str("\\\\"),
392 '\n' => out.push_str("\\n"),
393 '\r' => out.push_str("\\r"),
394 '\t' => out.push_str("\\t"),
395 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
396 c => out.push(c),
397 }
398 }
399 out.push('"');
400 out
401}
402
403fn is_allowed_root_attr_name(name: &str) -> bool {
410 matches!(name, "role" | "id" | "class" | "lang")
411 || name
412 .strip_prefix("data-")
413 .is_some_and(crate::aes::is_valid_data_attr_name)
414 || name
415 .strip_prefix("aria-")
416 .is_some_and(crate::aes::is_valid_data_attr_name)
417}
418
419pub(crate) fn validate_root_attrs(
429 requested: &[(String, String)],
430 existing: &[(String, String)],
431 label: &str,
432 warnings: &mut Vec<String>,
433) -> Vec<(String, String)> {
434 let built_in: Vec<&str> = existing.iter().map(|(k, _)| k.as_str()).collect();
435 let mut used: Vec<&str> = built_in.clone();
436 let mut out = Vec::new();
437 for (name, value) in requested {
438 if !is_allowed_root_attr_name(name) {
439 warnings.push(format!(
440 "{label}: svg_attr(\"{name}\") is not on the allow-list (data-*, aria-*, role, id, class, lang) — ignored"
441 ));
442 continue;
443 }
444 if used.contains(&name.as_str()) {
445 if built_in.contains(&name.as_str()) {
450 warnings.push(format!(
451 "{label}: svg_attr(\"{name}\") collides with an attribute ggplot-rs already writes to the root — ignored"
452 ));
453 } else {
454 warnings.push(format!(
455 "{label}: svg_attr(\"{name}\") was already set by an earlier svg_attr call — ignored"
456 ));
457 }
458 continue;
459 }
460 used.push(name.as_str());
461 out.push((name.clone(), value.clone()));
462 }
463 out
464}
465
466pub(crate) fn warnings_json(warnings: &[String]) -> String {
470 let items: Vec<String> = warnings.iter().map(|w| json_string(w)).collect();
471 format!("[{}]", items.join(","))
472}
473
474fn is_valid_mark_attr_name(name: &str) -> bool {
482 match name.strip_prefix("data-") {
483 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
484 None => false,
485 }
486}
487
488fn rgb((r, g, b): (u8, u8, u8)) -> String {
489 format!("#{r:02X}{g:02X}{b:02X}")
490}
491
492fn escape(s: &str) -> String {
499 let mut out = String::with_capacity(s.len());
500 for c in s.chars() {
501 match c {
502 '&' => out.push_str("&"),
503 '<' => out.push_str("<"),
504 '>' => out.push_str(">"),
505 '"' => out.push_str("""),
506 '\'' => out.push_str("'"),
507 '\t' | '\n' | '\r' => out.push(c),
508 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
509 c => out.push(c),
510 }
511 }
512 out
513}
514
515fn points(pts: &[(f64, f64)]) -> String {
516 pts.iter()
517 .map(|(x, y)| format!("{x:.2},{y:.2}"))
518 .collect::<Vec<_>>()
519 .join(" ")
520}
521
522fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
523 pts.iter()
524 .copied()
525 .filter(|(x, y)| x.is_finite() && y.is_finite())
526 .collect()
527}
528
529fn finite_runs(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
538 let mut runs = Vec::new();
539 let mut cur: Vec<(f64, f64)> = Vec::new();
540 for &(x, y) in pts {
541 if x.is_finite() && y.is_finite() {
542 cur.push((x, y));
543 } else if !cur.is_empty() {
544 if cur.len() >= 2 {
545 runs.push(std::mem::take(&mut cur));
546 } else {
547 cur.clear();
548 }
549 }
550 }
551 if cur.len() >= 2 {
552 runs.push(cur);
553 }
554 runs
555}
556
557impl DrawBackend for SvgBackend {
558 fn plot_area(&self) -> Rect {
559 self.plot_area.clone()
560 }
561 fn total_area(&self) -> Rect {
562 self.total_area.clone()
563 }
564
565 fn set_tooltip(&mut self, tooltip: Option<String>) {
566 self.tooltip = tooltip;
567 }
568
569 fn set_mark_axis(&mut self, key: Option<String>) {
570 self.axis_key = key;
571 }
572
573 fn set_mark_series(&mut self, series: Option<String>) {
574 self.series_key = series;
575 }
576
577 fn set_mark_value(&mut self, value: Option<String>) {
578 self.value_key = value;
579 }
580
581 fn set_mark_geom(&mut self, geom: Option<&str>) {
582 self.mark_geom = geom.map(str::to_string);
583 }
584
585 fn warn(&mut self, message: String) {
586 self.warnings.push(message);
587 }
588
589 fn set_mark_part(&mut self, part: Option<&str>) {
590 self.mark_part = part.map(|s| s.to_string());
591 }
592
593 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
594 self.mark_attrs = attrs;
595 }
596
597 fn wants_formatted_values(&self) -> bool {
598 self.formatted_values
599 }
600
601 fn set_mark_fvalue(&mut self, value: Option<String>) {
602 self.mark_fvalue = value;
603 }
604
605 fn set_mark_fx(&mut self, value: Option<String>) {
606 self.mark_fx = value;
607 }
608
609 fn draw_circle(
610 &mut self,
611 (cx, cy): (f64, f64),
612 radius: f64,
613 style: &PointStyle,
614 ) -> Result<(), RenderError> {
615 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
616 return Ok(());
617 }
618 let attrs = format!(
619 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\"{}",
620 radius.max(0.0),
621 rgb(style.color),
622 opacity_attr("fill", style.alpha)
623 );
624 self.push_mark("circle", "point", &attrs);
625 Ok(())
626 }
627
628 fn draw_shape(
632 &mut self,
633 (cx, cy): (f64, f64),
634 radius: f64,
635 style: &PointStyle,
636 ) -> Result<(), RenderError> {
637 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
638 return Ok(());
639 }
640 let r = radius.max(0.0);
641 let color = rgb(style.color);
642 let fill_opacity = opacity_attr("fill", style.alpha);
643 let stroke_opacity = opacity_attr("stroke", style.alpha);
644 let poly = |pts: &[(f64, f64)]| {
645 if style.filled {
646 format!("points=\"{}\" fill=\"{color}\"{fill_opacity}", points(pts))
647 } else {
648 format!(
649 "points=\"{}\" fill=\"none\" stroke=\"{color}\"{stroke_opacity}",
650 points(pts)
651 )
652 }
653 };
654 match style.shape {
655 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
656 PointShape::Square => {
657 let a = poly(&[
658 (cx - r, cy - r),
659 (cx + r, cy - r),
660 (cx + r, cy + r),
661 (cx - r, cy + r),
662 ]);
663 self.push_mark("polygon", "point", &a);
664 }
665 PointShape::Triangle => {
666 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
667 self.push_mark("polygon", "point", &a);
668 }
669 PointShape::Diamond => {
670 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
671 self.push_mark("polygon", "point", &a);
672 }
673 PointShape::Plus | PointShape::Cross => {
674 let d = if style.shape == PointShape::Plus {
675 format!(
676 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
677 cx - r,
678 cx + r,
679 cy - r,
680 cy + r
681 )
682 } else {
683 format!(
684 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
685 cx - r,
686 cy - r,
687 cx + r,
688 cy + r,
689 cx - r,
690 cy + r,
691 cx + r,
692 cy - r
693 )
694 };
695 let a = format!(
696 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\"{stroke_opacity}",
697 (r / 2.5).clamp(1.0, 3.0)
698 );
699 self.push_mark("path", "point", &a);
700 }
701 }
702 Ok(())
703 }
704
705 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
706 for run in finite_runs(pts) {
712 self.draw_line_run(&run, style);
713 }
714 Ok(())
715 }
716
717 fn draw_quadratic(
720 &mut self,
721 p0: (f64, f64),
722 ctrl: (f64, f64),
723 p1: (f64, f64),
724 style: &LineStyle,
725 ) -> Result<(), RenderError> {
726 if !(p0.0.is_finite()
727 && p0.1.is_finite()
728 && ctrl.0.is_finite()
729 && ctrl.1.is_finite()
730 && p1.0.is_finite()
731 && p1.1.is_finite())
732 {
733 return Ok(());
734 }
735 let dash = match style
736 .linetype
737 .pattern()
738 .iter()
739 .flat_map(|(d, g)| [*d, *g])
740 .map(|v| format!("{v}"))
741 .collect::<Vec<_>>()
742 .join(",")
743 {
744 s if s.is_empty() => String::new(),
745 s => format!(" stroke-dasharray=\"{s}\""),
746 };
747 let linecap = match style.lineend {
748 LineEnd::Butt => "",
749 LineEnd::Round => " stroke-linecap=\"round\"",
750 LineEnd::Square => " stroke-linecap=\"square\"",
751 };
752 let d = format!(
753 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
754 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
755 );
756 let attrs = format!(
757 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
758 rgb(style.color),
759 finite_or(style.width, 1.0),
760 opacity_attr("stroke", style.alpha),
761 dash,
762 linecap
763 );
764 self.push_mark("path", "line", &attrs);
765 Ok(())
766 }
767
768 fn draw_rect(
769 &mut self,
770 (x0, y0): (f64, f64),
771 (x1, y1): (f64, f64),
772 style: &RectStyle,
773 ) -> Result<(), RenderError> {
774 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
775 return Ok(());
776 }
777 let (x, y) = (x0.min(x1), y0.min(y1));
778 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
779 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
780 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
781 let attrs = format!(
782 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\" fill=\"{fill}\"{} \
783 stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
784 opacity_attr("fill", style.alpha),
785 finite_or(style.stroke_width, 0.0)
786 );
787 self.push_mark("rect", "bar", &attrs);
788 Ok(())
789 }
790
791 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
792 let pts = finite_pts(pts);
793 if pts.len() < 3 {
794 return Ok(());
795 }
796 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
797 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
798 let attrs = format!(
799 "points=\"{}\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
800 points(&pts),
801 opacity_attr("fill", style.alpha),
802 finite_or(style.stroke_width, 0.0)
803 );
804 self.push_mark("polygon", "band", &attrs);
805 Ok(())
806 }
807
808 fn draw_text(
809 &mut self,
810 text: &str,
811 (x, y): (f64, f64),
812 style: &TextStyle,
813 ) -> Result<(), RenderError> {
814 if !(x.is_finite() && y.is_finite()) {
815 return Ok(());
816 }
817 let anchor = match style.anchor {
818 TextAnchor::Start => "start",
819 TextAnchor::Middle => "middle",
820 TextAnchor::End => "end",
821 };
822 let family = escape(style.family.as_deref().unwrap_or("sans-serif"));
823 let weight = if style.face == FontFace::Bold {
824 " font-weight=\"bold\""
825 } else {
826 ""
827 };
828 let fstyle = if style.face == FontFace::Italic {
829 " font-style=\"italic\""
830 } else {
831 ""
832 };
833 let angle = finite_or(style.angle, 0.0);
834 let transform = if angle.abs() > 0.01 {
835 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
836 } else {
837 String::new()
838 };
839 let class = self.mark_class("text");
840 self.body.push_str(&format!(
841 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
842 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}{class}>{}</text>",
843 finite_or(style.size, 12.0),
844 rgb(style.color),
845 escape(text)
846 ));
847 Ok(())
848 }
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854 use crate::render::Rect;
855
856 fn backend() -> SvgBackend {
857 SvgBackend::new(
858 100,
859 100,
860 Rect {
861 x: 0.0,
862 y: 0.0,
863 width: 100.0,
864 height: 100.0,
865 },
866 )
867 }
868
869 #[test]
870 fn draw_quadratic_emits_a_path_with_m_and_q() {
871 let mut b = backend();
872 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
873 .unwrap();
874 let svg = b.finish();
875 assert!(svg.contains("<path"));
876 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
877 assert!(!svg.contains("NaN"));
878 assert!(!svg.contains("inf"));
879 }
880
881 #[test]
882 fn draw_quadratic_drops_non_finite_input() {
883 let mut b = backend();
884 b.draw_quadratic(
885 (f64::NAN, 0.0),
886 (10.0, 20.0),
887 (20.0, 0.0),
888 &LineStyle::default(),
889 )
890 .unwrap();
891 b.draw_quadratic(
892 (0.0, 0.0),
893 (f64::INFINITY, 20.0),
894 (20.0, 0.0),
895 &LineStyle::default(),
896 )
897 .unwrap();
898 let svg = b.finish();
899 assert!(!svg.contains("<path"));
900 }
901
902 #[test]
903 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
904 let mut b = backend();
905 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
906 .unwrap();
907 let svg = b.finish();
908 assert!(!svg.contains("stroke-linecap"));
909
910 let mut b2 = backend();
911 let style = LineStyle {
912 lineend: LineEnd::Round,
913 ..Default::default()
914 };
915 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
916 let svg2 = b2.finish();
917 assert!(svg2.contains("stroke-linecap=\"round\""));
918 }
919
920 #[test]
921 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
922 let style = LineStyle {
923 lineend: LineEnd::Square,
924 ..Default::default()
925 };
926 let mut b = backend();
927 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
928 assert!(b.finish().contains("stroke-linecap=\"square\""));
929
930 let mut b2 = backend();
931 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
932 .unwrap();
933 assert!(b2.finish().contains("stroke-linecap=\"square\""));
934 }
935
936 #[test]
941 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
942 let mut b = backend();
943 b.set_mark_attrs(vec![
944 ("not-data-prefixed".to_string(), "x".to_string()),
945 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
949 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
950 .unwrap();
951 let svg = b.finish();
952 assert!(!svg.contains("not-data-prefixed"));
953 assert!(!svg.contains("data-Upper"));
954 assert!(!svg.contains("data-=\""));
955 assert!(svg.contains("data-ok=\"valid\""));
956 }
957}