| 1 | /*! |
| 2 | A simple, streaming, partially-validating XML writer that writes XML data into an internal buffer. |
| 3 | |
| 4 | ## Features |
| 5 | |
| 6 | - A simple, bare-minimum, panic-based API. |
| 7 | - Non-allocating API. All methods are accepting either `fmt::Display` or `fmt::Arguments`. |
| 8 | - Nodes auto-closing. |
| 9 | |
| 10 | ## Example |
| 11 | |
| 12 | ```rust |
| 13 | use xmlwriter::*; |
| 14 | |
| 15 | let opt = Options { |
| 16 | use_single_quote: true, |
| 17 | ..Options::default() |
| 18 | }; |
| 19 | |
| 20 | let mut w = XmlWriter::new(opt); |
| 21 | w.start_element("svg" ); |
| 22 | w.write_attribute("xmlns" , "http://www.w3.org/2000/svg" ); |
| 23 | w.write_attribute_fmt("viewBox" , format_args!("{} {} {} {}" , 0, 0, 128, 128)); |
| 24 | w.start_element("text" ); |
| 25 | // We can write any object that implements `fmt::Display`. |
| 26 | w.write_attribute("x" , &10); |
| 27 | w.write_attribute("y" , &20); |
| 28 | w.write_text_fmt(format_args!("length is {}" , 5)); |
| 29 | |
| 30 | assert_eq!(w.end_document(), |
| 31 | "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'> |
| 32 | <text x='10' y='20'> |
| 33 | length is 5 |
| 34 | </text> |
| 35 | </svg> |
| 36 | " ); |
| 37 | ``` |
| 38 | */ |
| 39 | |
| 40 | #![doc (html_root_url = "https://docs.rs/xmlwriter/0.1.0" )] |
| 41 | |
| 42 | #![forbid (unsafe_code)] |
| 43 | #![warn (missing_docs)] |
| 44 | #![warn (missing_copy_implementations)] |
| 45 | |
| 46 | |
| 47 | use std::fmt::{self, Display}; |
| 48 | use std::io::Write; |
| 49 | use std::ops::Range; |
| 50 | |
| 51 | |
| 52 | /// An XML node indention. |
| 53 | #[derive (Clone, Copy, PartialEq, Debug)] |
| 54 | pub enum Indent { |
| 55 | /// Disable indention and new lines. |
| 56 | None, |
| 57 | /// Indent with spaces. Preferred range is 0..4. |
| 58 | Spaces(u8), |
| 59 | /// Indent with tabs. |
| 60 | Tabs, |
| 61 | } |
| 62 | |
| 63 | /// An XML writing options. |
| 64 | #[derive (Clone, Copy, Debug)] |
| 65 | pub struct Options { |
| 66 | /// Use single quote marks instead of double quote. |
| 67 | /// |
| 68 | /// # Examples |
| 69 | /// |
| 70 | /// Before: |
| 71 | /// |
| 72 | /// ```text |
| 73 | /// <rect fill="red"/> |
| 74 | /// ``` |
| 75 | /// |
| 76 | /// After: |
| 77 | /// |
| 78 | /// ```text |
| 79 | /// <rect fill='red'/> |
| 80 | /// ``` |
| 81 | /// |
| 82 | /// Default: disabled |
| 83 | pub use_single_quote: bool, |
| 84 | |
| 85 | /// Set XML nodes indention. |
| 86 | /// |
| 87 | /// # Examples |
| 88 | /// |
| 89 | /// `Indent::None` |
| 90 | /// Before: |
| 91 | /// |
| 92 | /// ```text |
| 93 | /// <svg> |
| 94 | /// <rect fill="red"/> |
| 95 | /// </svg> |
| 96 | /// ``` |
| 97 | /// |
| 98 | /// After: |
| 99 | /// |
| 100 | /// ```text |
| 101 | /// <svg><rect fill="red"/></svg> |
| 102 | /// ``` |
| 103 | /// |
| 104 | /// Default: 4 spaces |
| 105 | pub indent: Indent, |
| 106 | |
| 107 | /// Set XML attributes indention. |
| 108 | /// |
| 109 | /// # Examples |
| 110 | /// |
| 111 | /// `Indent::Spaces(2)` |
| 112 | /// |
| 113 | /// Before: |
| 114 | /// |
| 115 | /// ```text |
| 116 | /// <svg> |
| 117 | /// <rect fill="red" stroke="black"/> |
| 118 | /// </svg> |
| 119 | /// ``` |
| 120 | /// |
| 121 | /// After: |
| 122 | /// |
| 123 | /// ```text |
| 124 | /// <svg> |
| 125 | /// <rect |
| 126 | /// fill="red" |
| 127 | /// stroke="black"/> |
| 128 | /// </svg> |
| 129 | /// ``` |
| 130 | /// |
| 131 | /// Default: `None` |
| 132 | pub attributes_indent: Indent, |
| 133 | } |
| 134 | |
| 135 | impl Default for Options { |
| 136 | #[inline ] |
| 137 | fn default() -> Self { |
| 138 | Options { |
| 139 | use_single_quote: false, |
| 140 | indent: Indent::Spaces(4), |
| 141 | attributes_indent: Indent::None, |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | |
| 147 | #[derive (Clone, Copy, PartialEq, Debug)] |
| 148 | enum State { |
| 149 | Empty, |
| 150 | Document, |
| 151 | Attributes, |
| 152 | } |
| 153 | |
| 154 | struct DepthData { |
| 155 | range: Range<usize>, |
| 156 | has_children: bool, |
| 157 | } |
| 158 | |
| 159 | |
| 160 | /// An XML writer. |
| 161 | pub struct XmlWriter { |
| 162 | buf: Vec<u8>, |
| 163 | state: State, |
| 164 | preserve_whitespaces: bool, |
| 165 | depth_stack: Vec<DepthData>, |
| 166 | opt: Options, |
| 167 | } |
| 168 | |
| 169 | impl XmlWriter { |
| 170 | #[inline ] |
| 171 | fn from_vec(buf: Vec<u8>, opt: Options) -> Self { |
| 172 | XmlWriter { |
| 173 | buf, |
| 174 | state: State::Empty, |
| 175 | preserve_whitespaces: false, |
| 176 | depth_stack: Vec::new(), |
| 177 | opt, |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | /// Creates a new `XmlWriter`. |
| 182 | #[inline ] |
| 183 | pub fn new(opt: Options) -> Self { |
| 184 | Self::from_vec(Vec::new(), opt) |
| 185 | } |
| 186 | |
| 187 | /// Creates a new `XmlWriter` with a specified capacity. |
| 188 | #[inline ] |
| 189 | pub fn with_capacity(capacity: usize, opt: Options) -> Self { |
| 190 | Self::from_vec(Vec::with_capacity(capacity), opt) |
| 191 | } |
| 192 | |
| 193 | /// Writes an XML declaration. |
| 194 | /// |
| 195 | /// `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` |
| 196 | /// |
| 197 | /// # Panics |
| 198 | /// |
| 199 | /// - When called twice. |
| 200 | #[inline (never)] |
| 201 | pub fn write_declaration(&mut self) { |
| 202 | if self.state != State::Empty { |
| 203 | panic!("declaration was already written" ); |
| 204 | } |
| 205 | |
| 206 | // Pretend that we are writing an element. |
| 207 | self.state = State::Attributes; |
| 208 | |
| 209 | // <?xml version='1.0' encoding='UTF-8' standalone='yes'?> |
| 210 | self.push_str("<?xml" ); |
| 211 | self.write_attribute("version" , "1.0" ); |
| 212 | self.write_attribute("encoding" , "UTF-8" ); |
| 213 | self.write_attribute("standalone" , "no" ); |
| 214 | self.push_str("?>" ); |
| 215 | |
| 216 | self.state = State::Document; |
| 217 | } |
| 218 | |
| 219 | /// Writes a comment string. |
| 220 | pub fn write_comment(&mut self, text: &str) { |
| 221 | self.write_comment_fmt(format_args!(" {}" , text)); |
| 222 | } |
| 223 | |
| 224 | /// Writes a formatted comment. |
| 225 | #[inline (never)] |
| 226 | pub fn write_comment_fmt(&mut self, fmt: fmt::Arguments) { |
| 227 | if self.state == State::Attributes { |
| 228 | self.write_open_element(); |
| 229 | } |
| 230 | |
| 231 | if self.state != State::Empty { |
| 232 | self.write_new_line(); |
| 233 | } |
| 234 | |
| 235 | self.write_node_indent(); |
| 236 | |
| 237 | // <!--text--> |
| 238 | self.push_str("<!--" ); |
| 239 | self.buf.write_fmt(fmt).unwrap(); // TODO: check content |
| 240 | self.push_str("-->" ); |
| 241 | |
| 242 | if self.state == State::Attributes { |
| 243 | self.depth_stack.push(DepthData { |
| 244 | range: 0..0, |
| 245 | has_children: false, |
| 246 | }); |
| 247 | } |
| 248 | |
| 249 | self.state = State::Document; |
| 250 | } |
| 251 | |
| 252 | /// Starts writing a new element. |
| 253 | /// |
| 254 | /// This method writes only the `<tag-name` part. |
| 255 | #[inline (never)] |
| 256 | pub fn start_element(&mut self, name: &str) { |
| 257 | if self.state == State::Attributes { |
| 258 | self.write_open_element(); |
| 259 | } |
| 260 | |
| 261 | if self.state != State::Empty { |
| 262 | self.write_new_line(); |
| 263 | } |
| 264 | |
| 265 | if !self.preserve_whitespaces { |
| 266 | self.write_node_indent(); |
| 267 | } |
| 268 | |
| 269 | self.push_byte(b'<' ); |
| 270 | let start = self.buf.len(); |
| 271 | self.push_str(name); |
| 272 | |
| 273 | self.depth_stack.push(DepthData { |
| 274 | range: start..self.buf.len(), |
| 275 | has_children: false, |
| 276 | }); |
| 277 | |
| 278 | self.state = State::Attributes; |
| 279 | } |
| 280 | |
| 281 | /// Writes an attribute. |
| 282 | /// |
| 283 | /// Quotes in the value will be escaped. |
| 284 | /// |
| 285 | /// # Panics |
| 286 | /// |
| 287 | /// - When called before `start_element()`. |
| 288 | /// - When called after `close_element()`. |
| 289 | /// |
| 290 | /// # Example |
| 291 | /// |
| 292 | /// ``` |
| 293 | /// use xmlwriter::*; |
| 294 | /// |
| 295 | /// let mut w = XmlWriter::new(Options::default()); |
| 296 | /// w.start_element("svg" ); |
| 297 | /// w.write_attribute("x" , "5" ); |
| 298 | /// w.write_attribute("y" , &5); |
| 299 | /// assert_eq!(w.end_document(), "<svg x= \"5 \" y= \"5 \"/> \n" ); |
| 300 | /// ``` |
| 301 | pub fn write_attribute<V: Display + ?Sized>(&mut self, name: &str, value: &V) { |
| 302 | self.write_attribute_fmt(name, format_args!(" {}" , value)); |
| 303 | } |
| 304 | |
| 305 | /// Writes a formatted attribute value. |
| 306 | /// |
| 307 | /// Quotes in the value will be escaped. |
| 308 | /// |
| 309 | /// # Panics |
| 310 | /// |
| 311 | /// - When called before `start_element()`. |
| 312 | /// - When called after `close_element()`. |
| 313 | /// |
| 314 | /// # Example |
| 315 | /// |
| 316 | /// ``` |
| 317 | /// use xmlwriter::*; |
| 318 | /// |
| 319 | /// let mut w = XmlWriter::new(Options::default()); |
| 320 | /// w.start_element("rect" ); |
| 321 | /// w.write_attribute_fmt("fill" , format_args!("url(#{})" , "gradient" )); |
| 322 | /// assert_eq!(w.end_document(), "<rect fill= \"url(#gradient) \"/> \n" ); |
| 323 | /// ``` |
| 324 | #[inline (never)] |
| 325 | pub fn write_attribute_fmt(&mut self, name: &str, fmt: fmt::Arguments) { |
| 326 | if self.state != State::Attributes { |
| 327 | panic!("must be called after start_element()" ); |
| 328 | } |
| 329 | |
| 330 | self.write_attribute_prefix(name); |
| 331 | let start = self.buf.len(); |
| 332 | self.buf.write_fmt(fmt).unwrap(); |
| 333 | self.escape_attribute_value(start); |
| 334 | self.write_quote(); |
| 335 | } |
| 336 | |
| 337 | /// Writes a raw attribute value. |
| 338 | /// |
| 339 | /// Closure provides a mutable reference to an internal buffer. |
| 340 | /// |
| 341 | /// **Warning:** this method is an escape hatch for cases when you need to write |
| 342 | /// a lot of data very fast. |
| 343 | /// |
| 344 | /// # Panics |
| 345 | /// |
| 346 | /// - When called before `start_element()`. |
| 347 | /// - When called after `close_element()`. |
| 348 | /// |
| 349 | /// # Example |
| 350 | /// |
| 351 | /// ``` |
| 352 | /// use xmlwriter::*; |
| 353 | /// |
| 354 | /// let mut w = XmlWriter::new(Options::default()); |
| 355 | /// w.start_element("path" ); |
| 356 | /// w.write_attribute_raw("d" , |buf| buf.extend_from_slice(b"M 10 20 L 30 40" )); |
| 357 | /// assert_eq!(w.end_document(), "<path d= \"M 10 20 L 30 40 \"/> \n" ); |
| 358 | /// ``` |
| 359 | #[inline (never)] |
| 360 | pub fn write_attribute_raw<F>(&mut self, name: &str, f: F) |
| 361 | where F: FnOnce(&mut Vec<u8>) |
| 362 | { |
| 363 | if self.state != State::Attributes { |
| 364 | panic!("must be called after start_element()" ); |
| 365 | } |
| 366 | |
| 367 | self.write_attribute_prefix(name); |
| 368 | let start = self.buf.len(); |
| 369 | f(&mut self.buf); |
| 370 | self.escape_attribute_value(start); |
| 371 | self.write_quote(); |
| 372 | } |
| 373 | |
| 374 | #[inline (never)] |
| 375 | fn write_attribute_prefix(&mut self, name: &str) { |
| 376 | if self.opt.attributes_indent == Indent::None { |
| 377 | self.push_byte(b' ' ); |
| 378 | } else { |
| 379 | self.push_byte(b' \n' ); |
| 380 | |
| 381 | let depth = self.depth_stack.len(); |
| 382 | if depth > 0 { |
| 383 | self.write_indent(depth - 1, self.opt.indent); |
| 384 | } |
| 385 | |
| 386 | self.write_indent(1, self.opt.attributes_indent); |
| 387 | } |
| 388 | |
| 389 | self.push_str(name); |
| 390 | self.push_byte(b'=' ); |
| 391 | self.write_quote(); |
| 392 | } |
| 393 | |
| 394 | /// Escapes the attribute value string. |
| 395 | /// |
| 396 | /// - " -> " |
| 397 | /// - ' -> ' |
| 398 | #[inline (never)] |
| 399 | fn escape_attribute_value(&mut self, mut start: usize) { |
| 400 | let quote = if self.opt.use_single_quote { b' \'' } else { b'"' }; |
| 401 | while let Some(idx) = self.buf[start..].iter().position(|c| *c == quote) { |
| 402 | let i = start + idx; |
| 403 | let s = if self.opt.use_single_quote { b"'" } else { b""" }; |
| 404 | self.buf.splice(i..i+1, s.iter().cloned()); |
| 405 | start = i + 6; |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | /// Sets the preserve whitespaces flag. |
| 410 | /// |
| 411 | /// - If set, text nodes will be written as is. |
| 412 | /// - If not set, text nodes will be indented. |
| 413 | /// |
| 414 | /// Can be set at any moment. |
| 415 | /// |
| 416 | /// # Example |
| 417 | /// |
| 418 | /// ``` |
| 419 | /// use xmlwriter::*; |
| 420 | /// |
| 421 | /// let mut w = XmlWriter::new(Options::default()); |
| 422 | /// w.start_element("html" ); |
| 423 | /// w.start_element("p" ); |
| 424 | /// w.write_text("text" ); |
| 425 | /// w.end_element(); |
| 426 | /// w.start_element("p" ); |
| 427 | /// w.set_preserve_whitespaces(true); |
| 428 | /// w.write_text("text" ); |
| 429 | /// w.end_element(); |
| 430 | /// w.set_preserve_whitespaces(false); |
| 431 | /// assert_eq!(w.end_document(), |
| 432 | /// "<html> |
| 433 | /// <p> |
| 434 | /// text |
| 435 | /// </p> |
| 436 | /// <p>text</p> |
| 437 | /// </html> |
| 438 | /// " ); |
| 439 | /// ``` |
| 440 | pub fn set_preserve_whitespaces(&mut self, preserve: bool) { |
| 441 | self.preserve_whitespaces = preserve; |
| 442 | } |
| 443 | |
| 444 | /// Writes a text node. |
| 445 | /// |
| 446 | /// See `write_text_fmt()` for details. |
| 447 | pub fn write_text(&mut self, text: &str) { |
| 448 | self.write_text_fmt(format_args!(" {}" , text)); |
| 449 | } |
| 450 | |
| 451 | /// Writes a formatted text node. |
| 452 | /// |
| 453 | /// `<` will be escaped. |
| 454 | /// |
| 455 | /// # Panics |
| 456 | /// |
| 457 | /// - When called not after `start_element()`. |
| 458 | #[inline (never)] |
| 459 | pub fn write_text_fmt(&mut self, fmt: fmt::Arguments) { |
| 460 | if self.state == State::Empty || self.depth_stack.is_empty() { |
| 461 | panic!("must be called after start_element()" ); |
| 462 | } |
| 463 | |
| 464 | if self.state == State::Attributes { |
| 465 | self.write_open_element(); |
| 466 | } |
| 467 | |
| 468 | if self.state != State::Empty { |
| 469 | self.write_new_line(); |
| 470 | } |
| 471 | |
| 472 | self.write_node_indent(); |
| 473 | |
| 474 | let start = self.buf.len(); |
| 475 | self.buf.write_fmt(fmt).unwrap(); |
| 476 | self.escape_text(start); |
| 477 | |
| 478 | if self.state == State::Attributes { |
| 479 | self.depth_stack.push(DepthData { |
| 480 | range: 0..0, |
| 481 | has_children: false, |
| 482 | }); |
| 483 | } |
| 484 | |
| 485 | self.state = State::Document; |
| 486 | } |
| 487 | |
| 488 | fn escape_text(&mut self, mut start: usize) { |
| 489 | while let Some(idx) = self.buf[start..].iter().position(|c| *c == b'<' ) { |
| 490 | let i = start + idx; |
| 491 | self.buf.splice(i..i+1, b"<" .iter().cloned()); |
| 492 | start = i + 4; |
| 493 | } |
| 494 | } |
| 495 | |
| 496 | /// Closes an open element. |
| 497 | #[inline (never)] |
| 498 | pub fn end_element(&mut self) { |
| 499 | if let Some(depth) = self.depth_stack.pop() { |
| 500 | if depth.has_children { |
| 501 | if !self.preserve_whitespaces { |
| 502 | self.write_new_line(); |
| 503 | self.write_node_indent(); |
| 504 | } |
| 505 | |
| 506 | self.push_str("</" ); |
| 507 | |
| 508 | for i in depth.range { |
| 509 | self.push_byte(self.buf[i]); |
| 510 | } |
| 511 | |
| 512 | self.push_byte(b'>' ); |
| 513 | } else { |
| 514 | self.push_str("/>" ); |
| 515 | } |
| 516 | } |
| 517 | |
| 518 | self.state = State::Document; |
| 519 | } |
| 520 | |
| 521 | /// Closes all open elements and returns an internal XML buffer. |
| 522 | /// |
| 523 | /// # Example |
| 524 | /// |
| 525 | /// ``` |
| 526 | /// use xmlwriter::*; |
| 527 | /// |
| 528 | /// let mut w = XmlWriter::new(Options::default()); |
| 529 | /// w.start_element("svg" ); |
| 530 | /// w.start_element("g" ); |
| 531 | /// w.start_element("rect" ); |
| 532 | /// assert_eq!(w.end_document(), |
| 533 | /// "<svg> |
| 534 | /// <g> |
| 535 | /// <rect/> |
| 536 | /// </g> |
| 537 | /// </svg> |
| 538 | /// " ); |
| 539 | /// ``` |
| 540 | pub fn end_document(mut self) -> String { |
| 541 | while !self.depth_stack.is_empty() { |
| 542 | self.end_element(); |
| 543 | } |
| 544 | |
| 545 | self.write_new_line(); |
| 546 | |
| 547 | // The only way it can fail is if an invalid data |
| 548 | // was written via `write_attribute_raw()`. |
| 549 | String::from_utf8(self.buf).unwrap() |
| 550 | } |
| 551 | |
| 552 | #[inline ] |
| 553 | fn push_byte(&mut self, c: u8) { |
| 554 | self.buf.push(c); |
| 555 | } |
| 556 | |
| 557 | #[inline ] |
| 558 | fn push_str(&mut self, text: &str) { |
| 559 | self.buf.extend_from_slice(text.as_bytes()); |
| 560 | } |
| 561 | |
| 562 | #[inline ] |
| 563 | fn get_quote_char(&self) -> u8 { |
| 564 | if self.opt.use_single_quote { b' \'' } else { b'"' } |
| 565 | } |
| 566 | |
| 567 | #[inline ] |
| 568 | fn write_quote(&mut self) { |
| 569 | self.push_byte(self.get_quote_char()); |
| 570 | } |
| 571 | |
| 572 | fn write_open_element(&mut self) { |
| 573 | if let Some(depth) = self.depth_stack.last_mut() { |
| 574 | depth.has_children = true; |
| 575 | self.push_byte(b'>' ); |
| 576 | |
| 577 | self.state = State::Document; |
| 578 | } |
| 579 | } |
| 580 | |
| 581 | fn write_node_indent(&mut self) { |
| 582 | self.write_indent(self.depth_stack.len(), self.opt.indent); |
| 583 | } |
| 584 | |
| 585 | fn write_indent(&mut self, depth: usize, indent: Indent) { |
| 586 | if indent == Indent::None || self.preserve_whitespaces { |
| 587 | return; |
| 588 | } |
| 589 | |
| 590 | for _ in 0..depth { |
| 591 | match indent { |
| 592 | Indent::None => {} |
| 593 | Indent::Spaces(n) => { |
| 594 | for _ in 0..n { |
| 595 | self.push_byte(b' ' ); |
| 596 | } |
| 597 | } |
| 598 | Indent::Tabs => self.push_byte(b' \t' ), |
| 599 | } |
| 600 | } |
| 601 | } |
| 602 | |
| 603 | fn write_new_line(&mut self) { |
| 604 | if self.opt.indent != Indent::None && !self.preserve_whitespaces { |
| 605 | self.push_byte(b' \n' ); |
| 606 | } |
| 607 | } |
| 608 | } |
| 609 | |