use super::writer::Writer;
use crate::{
primitives::{
Length,
PositiveF32Pair,
},
utils::FloatWriter,
};
use std::{
fmt::{
Display,
Write,
},
marker::PhantomData,
};
use strict_num::{
FiniteF32,
NormalizedF32,
PositiveF32,
};
pub(crate) struct Initialized;
pub(crate) struct Opened;
mod private {
pub trait WriterState {}
}
impl private::WriterState for Initialized {}
impl private::WriterState for Opened {}
#[derive(Debug)]
pub(crate) struct ElementWriter<'a, T, S>
where
T: Write,
{
out: &'a mut T,
element_name: &'a str,
state: PhantomData<S>,
}
impl<T, S> FloatWriter<T> for ElementWriter<'_, T, S>
where
T: Write,
{
fn out_mut(&mut self) -> &mut T {
&mut self.out
}
}
impl<T, S> Writer<T> for ElementWriter<'_, T, S> where T: Write {}
impl<'a, T, S> ElementWriter<'a, T, S>
where
T: Write,
S: private::WriterState,
{
pub(crate) fn write<F>(self, write_fn: F) -> Result<Self, std::fmt::Error>
where
F: FnOnce(&mut T) -> std::fmt::Result,
{
write_fn(self.out).map(|_| self)
}
}
impl<'a, T> ElementWriter<'a, T, Initialized>
where
T: Write,
{
pub(crate) fn new(
out: &'a mut T,
element_name: &'a str,
) -> Result<ElementWriter<'a, T, Initialized>, std::fmt::Error> {
write!(out, "<{element_name}").map(|_| ElementWriter {
out,
element_name,
state: PhantomData,
})
}
pub(crate) fn attr<V>(self, key: &str, value: V) -> Result<Self, std::fmt::Error>
where
V: IntoAttrValue<T>,
{
value.write(key, self.out)?;
Ok(self)
}
pub(crate) fn attrs<'b, I, V>(self, attrs: I) -> Result<Self, std::fmt::Error>
where
V: IntoAttrValue<T>,
I: IntoIterator<Item = (&'b str, V)>,
{
attrs
.into_iter()
.try_for_each(|(key, value)| value.write(key, self.out))
.map(|_| self)
}
pub(crate) fn attr_if<V>(
mut self,
key: &str,
value: V,
condition: bool,
) -> Result<Self, std::fmt::Error>
where
V: IntoAttrValue<T>,
{
if condition {
self = self.attr(key, value)?;
}
Ok(self)
}
pub(crate) fn write_attr<F>(self, key: &str, write_fn: F) -> Result<Self, std::fmt::Error>
where
F: FnOnce(&mut T) -> std::fmt::Result,
{
write!(self.out, r#" {key}=""#)?;
write_fn(self.out)?;
self.out.write_char('"').map(|_| self)
}
pub(crate) fn open(self) -> Result<ElementWriter<'a, T, Opened>, std::fmt::Error> {
self.out.write_char('>').map(|_| ElementWriter {
out: self.out,
element_name: self.element_name,
state: PhantomData,
})
}
pub(crate) fn content<F>(
self,
write_fn: F,
) -> Result<ElementWriter<'a, T, Opened>, std::fmt::Error>
where
F: FnOnce(&mut T) -> std::fmt::Result,
{
let writer = self.open()?;
writer.content(write_fn)
}
pub(crate) fn close(self) -> std::fmt::Result {
self.out.write_str(" />")
}
pub(crate) fn close_tag(out: &'a mut T, element_name: &str) -> std::fmt::Result {
write!(out, "</{element_name}>")
}
}
impl<'a, T> ElementWriter<'a, T, Opened>
where
T: Write,
{
pub(crate) fn content<F>(self, write_fn: F) -> Result<Self, std::fmt::Error>
where
F: FnOnce(&mut T) -> std::fmt::Result,
{
self.write(write_fn)
}
pub(crate) fn close(self) -> std::fmt::Result {
write!(self.out, "</{}>", self.element_name)
}
}
pub(crate) trait IntoAttrValue<W>
where
W: Write,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result;
}
impl<'a, W, D> IntoAttrValue<W> for (D,)
where
W: Write,
D: Display,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
write!(out, r#" {key}="{}""#, self.0)
}
}
impl<'a, W> IntoAttrValue<W> for &'a str
where
W: Write,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
write!(out, r#" {key}="{self}""#)
}
}
impl<'a, W> IntoAttrValue<W> for String
where
W: Write,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
IntoAttrValue::write(&self.as_str(), key, out)
}
}
impl<'a, W> IntoAttrValue<W> for f32
where
W: Write,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
write!(out, r#" {key}=""#)?;
out.write_float(*self)?;
out.write_char('"')
}
}
impl<'a, W, const AUTO: bool, const PERCENT: bool> IntoAttrValue<W> for Length<AUTO, PERCENT>
where
W: Write,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
IntoAttrValue::write(&(self,), key, out)
}
}
impl<'a, W> IntoAttrValue<W> for FiniteF32
where
W: Write,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
IntoAttrValue::write(&self.get(), key, out)
}
}
impl<'a, W> IntoAttrValue<W> for NormalizedF32
where
W: Write,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
IntoAttrValue::write(&self.get(), key, out)
}
}
impl<'a, W> IntoAttrValue<W> for PositiveF32
where
W: Write,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
IntoAttrValue::write(&self.get(), key, out)
}
}
impl<'a, W> IntoAttrValue<W> for PositiveF32Pair
where
W: Write,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
write!(out, r#" {key}="{self}""#)
}
}
impl<'a, W, V> IntoAttrValue<W> for Option<V>
where
W: Write,
V: IntoAttrValue<W>,
{
fn write(&self, key: &str, out: &mut W) -> std::fmt::Result {
if let Some(value) = self {
IntoAttrValue::write(value, key, out)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
macros::{
ff32,
nf32,
pf32,
},
test_utils::{
assert_xml,
str_sink,
},
};
#[test]
fn writes_empty_element() {
assert_xml(
str_sink(|out| ElementWriter::new(out, "path")?.close()),
r#"<path />"#,
);
}
#[test]
fn writes_single_attr() {
assert_xml(
str_sink(|out| ElementWriter::new(out, "rect")?.attr("id", "test")?.close()),
r#"<rect id="test" />"#,
);
}
#[test]
fn writes_multiple_attrs() {
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "rect")?
.attrs([("x", 10.0), ("y", 25.0), ("width", 96.0), ("height", 64.0)])?
.close()
}),
r#"<rect x="10" y="25" width="96" height="64" />"#,
);
}
#[test]
fn attr_if_writes_conditionally() {
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "circle")?
.attr_if("cx", 50.0, true)?
.attr_if("cy", 50.0, false)?
.close()
}),
r#"<circle cx="50" />"#,
);
}
#[test]
fn element_with_content() {
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "text")?
.open()?
.content(|out| out.write_str("hello"))?
.close()
}),
r#"<text>hello</text>"#,
);
}
#[test]
fn content_without_manually_opening() {
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "text")?
.content(|out| out.write_str("hello"))?
.close()
}),
r#"<text>hello</text>"#,
);
}
#[test]
fn custom_attr_writer() {
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "polygon")?
.write_attr("points", |out| out.write_str("0,0 10,10"))?
.close()
}),
r#"<polygon points="0,0 10,10" />"#,
);
}
#[test]
fn writes_float_attrs() {
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "group")?
.attr("x", nf32!(0.5))?
.attr("y", pf32!(1.5))?
.attr("z", ff32!(2.5))?
.close()
}),
r#"<group x="0.5" y="1.5" z="2.5" />"#,
);
}
#[test]
fn writes_length_attr() {
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "rect")?
.attr("width", Length::<false, false>::units(100.0))?
.close()
}),
r#"<rect width="100" />"#,
);
}
#[test]
fn writes_positive_f32_pair() {
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "feMorphology")?
.attr("radius", PositiveF32Pair::from((1.5, 2.5)))?
.close()
}),
r#"<feMorphology radius="1.5 2.5" />"#,
);
}
#[test]
fn writes_optional_attr() {
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "rect")?
.attr("x", Some(10.0))?
.close()
}),
r#"<rect x="10" />"#,
);
assert_xml(
str_sink(|out| {
ElementWriter::new(out, "rect")?
.attr("x", None::<f32>)?
.close()
}),
"<rect />",
);
}
#[test]
fn writes_close_tag() {
assert_eq!(str_sink(|out| ElementWriter::close_tag(out, "g")), "</g>");
}
}