1use super::{
2 attributes::Attributes,
3 document::Document,
4 node::{ElementData, NodeData, NodeId},
5 DocumentStyleMap, InliningMode,
6};
7use crate::{html::ElementStyleMap, parser, InlineError};
8use html5ever::{local_name, ns, tendril::StrTendril, LocalName, QualName};
9use memchr::{memchr3_iter, memchr_iter};
10use smallvec::{smallvec, SmallVec};
11use std::io::Write;
12
13#[inline]
15fn supports_dimension_attrs(name: &LocalName) -> bool {
16 matches!(
17 *name,
18 local_name!("table") | local_name!("td") | local_name!("th") | local_name!("img")
19 )
20}
21
22#[inline]
24fn is_table_element(name: &LocalName) -> bool {
25 matches!(
26 *name,
27 local_name!("table") | local_name!("td") | local_name!("th")
28 )
29}
30
31#[derive(Clone, Copy)]
33enum DimensionValue<'a> {
34 Numeric(&'a str),
36 Percent(&'a str),
38 Auto,
40}
41
42impl DimensionValue<'_> {
43 #[inline]
44 fn write_to<W: Write>(&self, writer: &mut W) -> Result<(), InlineError> {
45 match self {
46 DimensionValue::Numeric(n) => writer.write_all(n.as_bytes())?,
47 DimensionValue::Percent(n) => {
48 writer.write_all(n.as_bytes())?;
49 writer.write_all(b"%")?;
50 }
51 DimensionValue::Auto => writer.write_all(b"auto")?,
52 }
53 Ok(())
54 }
55}
56
57#[inline]
59#[allow(clippy::arithmetic_side_effects)]
60fn extract_dimension_value(value: &str, allow_percent: bool) -> Option<DimensionValue<'_>> {
61 let value = value.trim();
62
63 if value.eq_ignore_ascii_case("auto") {
64 return Some(DimensionValue::Auto);
65 }
66
67 let bytes = value.as_bytes();
69 let mut end = 0;
70 let mut has_dot = false;
71
72 if bytes.first() == Some(&b'-') || bytes.first() == Some(&b'+') {
74 end = 1;
75 }
76
77 while end < bytes.len() {
79 match bytes[end] {
80 b'0'..=b'9' => end += 1,
81 b'.' if !has_dot => {
82 has_dot = true;
83 end += 1;
84 }
85 _ => break,
86 }
87 }
88
89 if end == 0 || (end == 1 && (bytes[0] == b'-' || bytes[0] == b'+')) {
91 return None;
92 }
93
94 let numeric_part = &value[..end];
95 let unit_part = value[end..].trim();
97 let unit_part = unit_part
99 .strip_suffix("!important")
100 .map_or(unit_part, str::trim);
101
102 match unit_part {
103 "" | "px" => Some(DimensionValue::Numeric(numeric_part)),
105 "%" if allow_percent => Some(DimensionValue::Percent(numeric_part)),
107 _ => None,
109 }
110}
111
112#[inline]
114fn find_style_value<'a>(styles: &'a ElementStyleMap<'_>, property: &str) -> Option<&'a str> {
115 styles
116 .iter()
117 .rev()
118 .find(|(name, _, _)| *name == property)
119 .map(|(_, _, value)| *value)
120}
121
122#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
123pub(crate) fn serialize_to<W: Write>(
124 document: &Document,
125 writer: &mut W,
126 styles: DocumentStyleMap<'_>,
127 keep_style_tags: bool,
128 keep_link_tags: bool,
129 minify_css: bool,
130 at_rules: Option<&String>,
131 mode: InliningMode,
132 apply_width_attributes: bool,
133 apply_height_attributes: bool,
134) -> Result<(), InlineError> {
135 let sink = Sink::new(
136 document,
137 NodeId::document_id(),
138 keep_style_tags,
139 keep_link_tags,
140 minify_css,
141 at_rules,
142 mode,
143 apply_width_attributes,
144 apply_height_attributes,
145 );
146 let mut ser = HtmlSerializer::new(writer, styles);
147 sink.serialize(&mut ser)
148}
149
150#[allow(clippy::struct_excessive_bools)]
152struct Sink<'a> {
153 document: &'a Document,
154 node: NodeId,
155 keep_style_tags: bool,
156 keep_link_tags: bool,
157 minify_css: bool,
158 at_rules: Option<&'a String>,
159 inlining_mode: InliningMode,
160 apply_width_attributes: bool,
161 apply_height_attributes: bool,
162}
163
164impl<'a> Sink<'a> {
165 #[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
166 fn new(
167 document: &'a Document,
168 node: NodeId,
169 keep_style_tags: bool,
170 keep_link_tags: bool,
171 minify_css: bool,
172 at_rules: Option<&'a String>,
173 inlining_mode: InliningMode,
174 apply_width_attributes: bool,
175 apply_height_attributes: bool,
176 ) -> Sink<'a> {
177 Sink {
178 document,
179 node,
180 keep_style_tags,
181 keep_link_tags,
182 minify_css,
183 at_rules,
184 inlining_mode,
185 apply_width_attributes,
186 apply_height_attributes,
187 }
188 }
189 #[inline]
190 fn for_node(&self, node: NodeId) -> Sink<'a> {
191 Sink::new(
192 self.document,
193 node,
194 self.keep_style_tags,
195 self.keep_link_tags,
196 self.minify_css,
197 self.at_rules,
198 self.inlining_mode,
199 self.apply_width_attributes,
200 self.apply_height_attributes,
201 )
202 }
203 #[inline]
204 fn data(&self) -> &NodeData {
205 &self.document[self.node].data
206 }
207 #[inline]
208 fn should_skip_element(&self, element: &ElementData) -> bool {
209 if element.name.local == local_name!("style") {
210 !self.keep_style_tags && element.attributes.get_css_inline() != Some("keep")
211 } else if element.name.local == local_name!("link")
212 && element.attributes.get(local_name!("rel")) == Some("stylesheet")
213 {
214 !self.keep_link_tags
215 } else if element.name.local == local_name!("html") {
216 matches!(self.inlining_mode, InliningMode::Fragment)
217 } else {
218 false
219 }
220 }
221
222 fn serialize_children<W: Write>(
223 &self,
224 serializer: &mut HtmlSerializer<'_, W>,
225 ) -> Result<(), InlineError> {
226 for child in self.document.children(self.node) {
227 self.for_node(child).serialize(serializer)?;
228 }
229 Ok(())
230 }
231
232 fn serialize<W: Write>(
233 &self,
234 serializer: &mut HtmlSerializer<'_, W>,
235 ) -> Result<(), InlineError> {
236 match self.data() {
237 NodeData::Element {
238 element,
239 inlining_ignored,
240 } => {
241 if self.should_skip_element(element) {
242 return Ok(());
243 }
244
245 let style_node_id = if *inlining_ignored {
246 None
247 } else {
248 Some(self.node)
249 };
250
251 serializer.start_elem(
252 &element.name,
253 &element.attributes,
254 style_node_id,
255 self.minify_css,
256 self.apply_width_attributes,
257 self.apply_height_attributes,
258 )?;
259
260 if element.name.local == local_name!("head") {
261 if let Some(at_rules) = &self.at_rules {
262 if !at_rules.is_empty() {
263 serializer.write_at_rules_style(at_rules)?;
264 }
265 }
266 }
267
268 self.serialize_children(serializer)?;
269
270 serializer.end_elem(&element.name)?;
271 Ok(())
272 }
273 NodeData::Document => self.serialize_children(serializer),
274 NodeData::Doctype { name } => serializer.write_doctype(name),
275 NodeData::Text { text } => serializer.write_text(text),
276 NodeData::Comment { text } => serializer.write_comment(text),
277 NodeData::ProcessingInstruction { target, data } => {
278 serializer.write_processing_instruction(target, data)
279 }
280 }
281 }
282}
283
284struct ElemInfo {
285 html_name: Option<LocalName>,
286 ignore_children: bool,
287}
288
289struct HtmlSerializer<'a, Wr: Write> {
292 writer: Wr,
293 styles: DocumentStyleMap<'a>,
294 stack: Vec<ElemInfo>,
295 style_buffer: SmallVec<[Vec<u8>; 8]>,
296}
297
298impl<'a, W: Write> HtmlSerializer<'a, W> {
299 fn new(writer: W, styles: DocumentStyleMap<'a>) -> Self {
300 let mut stack = Vec::with_capacity(8);
301 stack.push(ElemInfo {
302 html_name: None,
303 ignore_children: false,
304 });
305 HtmlSerializer {
306 writer,
307 styles,
308 stack,
309 style_buffer: smallvec![],
310 }
311 }
312
313 fn parent(&mut self) -> &mut ElemInfo {
314 self.stack.last_mut().expect("no parent ElemInfo")
315 }
316
317 fn write_escaped(&mut self, text: &str) -> Result<(), InlineError> {
318 let mut last_end = 0;
319 for (start, part) in text.match_indices(['&', '\u{00A0}', '<', '>']) {
320 self.writer.write_all(
321 text.get(last_end..start)
322 .expect("Invalid substring")
323 .as_bytes(),
324 )?;
325 match (part.as_bytes()[0] & 0b0000_1110) >> 1 {
328 1 => self.writer.write_all(b" ")?,
329 3 => self.writer.write_all(b"&")?,
330 6 => self.writer.write_all(b"<")?,
331 7 => self.writer.write_all(b">")?,
332 _ => unreachable!(),
333 }
334 last_end = start.checked_add(part.len()).expect("Size overflow");
335 }
336 self.writer.write_all(
337 text.get(last_end..text.len())
338 .expect("Invalid substring")
339 .as_bytes(),
340 )?;
341 Ok(())
342 }
343
344 #[allow(clippy::arithmetic_side_effects)]
345 fn write_attributes(&mut self, text: &str) -> Result<(), InlineError> {
346 let bytes = text.as_bytes();
347 let mut last_end = 0;
348
349 for idx in memchr3_iter(b'&', b'"', 0xC2, bytes) {
351 match bytes[idx] {
352 b'&' => {
353 self.writer.write_all(&bytes[last_end..idx])?;
354 self.writer.write_all(b"&")?;
355 last_end = idx + 1;
356 }
357 b'"' => {
358 self.writer.write_all(&bytes[last_end..idx])?;
359 self.writer.write_all(b""")?;
360 last_end = idx + 1;
361 }
362 0xC2 if bytes.get(idx + 1) == Some(&0xA0) => {
363 self.writer.write_all(&bytes[last_end..idx])?;
364 self.writer.write_all(b" ")?;
365 last_end = idx + 2; }
367 _ => {} }
369 }
370 self.writer.write_all(&bytes[last_end..])?;
371 Ok(())
372 }
373
374 #[allow(clippy::too_many_lines)]
375 fn start_elem(
376 &mut self,
377 name: &QualName,
378 attrs: &Attributes,
379 style_node_id: Option<NodeId>,
380 minify_css: bool,
381 apply_width_attributes: bool,
382 apply_height_attributes: bool,
383 ) -> Result<(), InlineError> {
384 let html_name = match name.ns {
385 ns!(html) => Some(name.local.clone()),
386 _ => None,
387 };
388
389 if self.parent().ignore_children {
390 self.stack.push(ElemInfo {
391 html_name,
392 ignore_children: true,
393 });
394 return Ok(());
395 }
396
397 let mut styles = if let Some(node_id) = style_node_id {
398 self.styles.get_mut(node_id.get()).and_then(|slot| {
399 slot.take().map(|mut styles| {
400 styles.sort_unstable_by(|a, b| a.1.cmp(&b.1));
402 styles
403 })
404 })
405 } else {
406 None
407 };
408
409 self.writer.write_all(b"<")?;
410 self.writer.write_all(name.local.as_bytes())?;
411 if let Some(class) = &attrs.class {
412 self.writer.write_all(b" class=\"")?;
413 self.writer.write_all(class.value.as_bytes())?;
414 self.writer.write_all(b"\"")?;
415 }
416
417 if let Some(ref html_name) = html_name {
419 if supports_dimension_attrs(html_name) {
420 let allow_percent = is_table_element(html_name);
421 if apply_width_attributes && !attrs.contains(local_name!("width")) {
422 if let Some(dim) = styles
423 .as_ref()
424 .and_then(|s| find_style_value(s, "width"))
425 .and_then(|v| extract_dimension_value(v, allow_percent))
426 {
427 self.writer.write_all(b" width=\"")?;
428 dim.write_to(&mut self.writer)?;
429 self.writer.write_all(b"\"")?;
430 }
431 }
432 if apply_height_attributes && !attrs.contains(local_name!("height")) {
433 if let Some(dim) = styles
434 .as_ref()
435 .and_then(|s| find_style_value(s, "height"))
436 .and_then(|v| extract_dimension_value(v, allow_percent))
437 {
438 self.writer.write_all(b" height=\"")?;
439 dim.write_to(&mut self.writer)?;
440 self.writer.write_all(b"\"")?;
441 }
442 }
443 }
444 }
445
446 for attr in &attrs.attributes {
447 self.writer.write_all(b" ")?;
448
449 match attr.name.ns {
450 ns!() => (),
451 ns!(xml) => self.writer.write_all(b"xml:")?,
452 ns!(xmlns) => {
453 if attr.name.local != local_name!("xmlns") {
454 self.writer.write_all(b"xmlns:")?;
455 }
456 }
457 ns!(xlink) => self.writer.write_all(b"xlink:")?,
458 _ => {
459 self.writer.write_all(b"unknown_namespace:")?;
460 }
461 }
462
463 self.writer.write_all(attr.name.local.as_bytes())?;
464 self.writer.write_all(b"=\"")?;
465 if attr.name.local == local_name!("style") {
466 if let Some(new_styles) = &styles {
467 merge_styles(
468 &mut self.writer,
469 &attr.value,
470 new_styles,
471 &mut self.style_buffer,
472 minify_css,
473 )?;
474 styles = None;
475 } else {
476 self.write_attributes(&attr.value)?;
477 }
478 } else {
479 self.write_attributes(&attr.value)?;
480 }
481 self.writer.write_all(b"\"")?;
482 }
483 if let Some(styles) = styles {
484 self.writer.write_all(b" style=\"")?;
485 if minify_css {
486 let mut it = styles.iter().peekable();
487 while let Some((property, _, value)) = it.next() {
488 write_declaration(&mut self.writer, property, value, minify_css)?;
489 if !minify_css || it.peek().is_some() {
490 self.writer.write_all(b";")?;
491 }
492 }
493 } else {
494 for (property, _, value) in styles {
495 write_declaration(&mut self.writer, property, value, minify_css)?;
496 self.writer.write_all(b";")?;
497 }
498 }
499 self.writer.write_all(b"\"")?;
500 }
501 self.writer.write_all(b">")?;
502
503 let ignore_children = name.ns == ns!(html)
504 && matches!(
505 name.local,
506 local_name!("area")
507 | local_name!("base")
508 | local_name!("basefont")
509 | local_name!("bgsound")
510 | local_name!("br")
511 | local_name!("col")
512 | local_name!("embed")
513 | local_name!("frame")
514 | local_name!("hr")
515 | local_name!("img")
516 | local_name!("input")
517 | local_name!("keygen")
518 | local_name!("link")
519 | local_name!("meta")
520 | local_name!("param")
521 | local_name!("source")
522 | local_name!("track")
523 | local_name!("wbr")
524 );
525
526 self.stack.push(ElemInfo {
527 html_name,
528 ignore_children,
529 });
530
531 Ok(())
532 }
533
534 fn end_elem(&mut self, name: &QualName) -> Result<(), InlineError> {
535 let Some(info) = self.stack.pop() else {
536 panic!("no ElemInfo")
537 };
538 if info.ignore_children {
539 return Ok(());
540 }
541
542 self.writer.write_all(b"</")?;
543 self.writer.write_all(name.local.as_bytes())?;
544 self.writer.write_all(b">")?;
545 Ok(())
546 }
547
548 fn write_text(&mut self, text: &str) -> Result<(), InlineError> {
549 let escape = !matches!(
550 self.parent().html_name,
551 Some(
552 local_name!("style")
553 | local_name!("script")
554 | local_name!("xmp")
555 | local_name!("iframe")
556 | local_name!("noembed")
557 | local_name!("noframes")
558 | local_name!("plaintext")
559 | local_name!("noscript")
560 ),
561 );
562
563 if escape {
564 self.write_escaped(text)?;
565 } else {
566 self.writer.write_all(text.as_bytes())?;
567 }
568 Ok(())
569 }
570
571 fn write_at_rules_style(&mut self, at_rules: &str) -> Result<(), InlineError> {
572 self.writer.write_all(b"<style>")?;
573 self.writer.write_all(at_rules.as_bytes())?;
574 self.writer.write_all(b"</style>")?;
575 Ok(())
576 }
577
578 fn write_comment(&mut self, text: &str) -> Result<(), InlineError> {
579 self.writer.write_all(b"<!--")?;
580 self.writer.write_all(text.as_bytes())?;
581 self.writer.write_all(b"-->")?;
582 Ok(())
583 }
584
585 fn write_doctype(&mut self, name: &str) -> Result<(), InlineError> {
586 self.writer.write_all(b"<!DOCTYPE ")?;
587 self.writer.write_all(name.as_bytes())?;
588 self.writer.write_all(b">")?;
589 Ok(())
590 }
591
592 fn write_processing_instruction(
593 &mut self,
594 target: &str,
595 data: &str,
596 ) -> Result<(), InlineError> {
597 self.writer.write_all(b"<?")?;
598 self.writer.write_all(target.as_bytes())?;
599 self.writer.write_all(b" ")?;
600 self.writer.write_all(data.as_bytes())?;
601 self.writer.write_all(b">")?;
602 Ok(())
603 }
604}
605
606const STYLE_SEPARATOR: &[u8] = b": ";
607const STYLE_SEPARATOR_MIN: &[u8] = b":";
608
609#[inline]
610fn write_declaration<Wr: Write>(
611 writer: &mut Wr,
612 name: &str,
613 value: &str,
614 minify_css: bool,
615) -> Result<(), InlineError> {
616 writer.write_all(name.as_bytes())?;
617 if minify_css {
618 writer.write_all(STYLE_SEPARATOR_MIN)?;
619 } else {
620 writer.write_all(STYLE_SEPARATOR)?;
621 }
622 write_declaration_value(writer, value)
623}
624
625#[inline]
626#[allow(clippy::arithmetic_side_effects)]
627fn write_declaration_value<Wr: Write>(writer: &mut Wr, value: &str) -> Result<(), InlineError> {
628 let value = value.trim();
629 let bytes = value.as_bytes();
630
631 let mut last_end = 0;
632 for idx in memchr_iter(b'"', bytes) {
633 writer.write_all(&bytes[last_end..idx])?;
634 writer.write_all(b"'")?;
635 last_end = idx + 1;
636 }
637 writer.write_all(&bytes[last_end..])?;
638 Ok(())
639}
640
641macro_rules! push_or_update {
642 ($style_buffer:expr, $length:expr, $name: expr, $value:expr, $minify_css:expr) => {{
643 if let Some(style) = $style_buffer.get_mut($length) {
644 style.clear();
645 write_declaration(style, &$name, $value, $minify_css)?;
646 } else {
647 let value = $value.trim();
648 let mut style = Vec::with_capacity(
649 $name
650 .len()
651 .saturating_add(STYLE_SEPARATOR.len())
652 .saturating_add(value.len()),
653 );
654 write_declaration(&mut style, &$name, $value, $minify_css)?;
655 $style_buffer.push(style);
656 };
657 $length = $length.saturating_add(1);
658 }};
659}
660
661fn merge_styles<Wr: Write>(
665 writer: &mut Wr,
666 current_style: &StrTendril,
667 new_styles: &ElementStyleMap<'_>,
668 declarations_buffer: &mut SmallVec<[Vec<u8>; 8]>,
669 minify_css: bool,
670) -> Result<(), InlineError> {
671 let mut parser_input = cssparser::ParserInput::new(current_style);
674 let mut parser = cssparser::Parser::new(&mut parser_input);
675 let mut declaration_parser = parser::CSSDeclarationListParser;
676 let current_declarations = cssparser::RuleBodyParser::new(&mut parser, &mut declaration_parser);
677 let mut parsed_declarations_count: usize = 0;
680 for (idx, declaration) in current_declarations.enumerate() {
681 parsed_declarations_count = parsed_declarations_count.saturating_add(1);
682 let (property, value) = declaration?;
683 let estimated_declaration_size = property
684 .len()
685 .saturating_add(STYLE_SEPARATOR.len())
686 .saturating_add(value.len());
687 if let Some(buffer) = declarations_buffer.get_mut(idx) {
690 buffer.clear();
691 buffer.reserve(estimated_declaration_size);
692 write_declaration(buffer, &property, value, minify_css)?;
693 } else {
694 let mut buffer = Vec::with_capacity(estimated_declaration_size);
695 write_declaration(&mut buffer, &property, value, minify_css)?;
696 declarations_buffer.push(buffer);
697 }
698 }
699 let current_declarations_count = parsed_declarations_count;
701 let sep = if minify_css {
704 STYLE_SEPARATOR_MIN
705 } else {
706 STYLE_SEPARATOR
707 };
708 for (property, _, value) in new_styles {
709 match (
710 value.trim_end().strip_suffix("!important"),
711 declarations_buffer
712 .iter_mut()
713 .take(parsed_declarations_count)
714 .find(|style| {
715 style.starts_with(property.as_bytes())
716 && style.get(property.len()..property.len().saturating_add(sep.len()))
717 == Some(sep)
718 }),
719 ) {
720 (Some(value), Some(buffer)) => {
724 if !buffer.ends_with(b"!important") {
725 buffer.truncate(property.len().saturating_add(sep.len()));
726 write_declaration_value(buffer, value)?;
727 buffer.extend_from_slice(b" !important");
728 }
729 }
730 (Some(value), None) => {
732 push_or_update!(
733 declarations_buffer,
734 parsed_declarations_count,
735 property,
736 value,
737 minify_css
738 );
739 if let Some(buf) =
742 declarations_buffer.get_mut(parsed_declarations_count.saturating_sub(1))
743 {
744 buf.extend_from_slice(b" !important");
745 }
746 }
747 (None, None) => push_or_update!(
750 declarations_buffer,
751 parsed_declarations_count,
752 property,
753 value,
754 minify_css
755 ),
756 (None, Some(_)) => {}
759 }
760 }
761
762 let mut first = true;
763 for range in [
764 current_declarations_count..parsed_declarations_count,
766 0..current_declarations_count,
768 ] {
769 for declaration in &declarations_buffer[range] {
770 if first {
771 first = false;
772 } else {
773 writer.write_all(b";")?;
774 }
775 writer.write_all(declaration)?;
776 }
777 }
778 Ok(())
779}
780
781#[cfg(test)]
782mod tests {
783 use crate::html::InliningMode;
784
785 use super::Document;
786
787 #[test]
788 fn test_serialize() {
789 let doc = Document::parse_with_options(
790 b"<html><head><style>h1 { color:blue; }</style><style>h1 { color:red }</style></head>",
791 0,
792 InliningMode::Document,
793 );
794 let mut buffer = Vec::new();
795 doc.serialize(
796 &mut buffer,
797 vec![None; doc.nodes.len()],
798 true,
799 false,
800 false,
801 None,
802 InliningMode::Document,
803 false,
804 false,
805 )
806 .expect("Should not fail");
807 assert_eq!(buffer, b"<html><head><style>h1 { color:blue; }</style><style>h1 { color:red }</style></head><body></body></html>");
808 }
809
810 #[test]
811 fn test_skip_style_tags() {
812 let doc = Document::parse_with_options(
813 b"<html><head><style>h1 { color:blue; }</style><style>h1 { color:red }</style></head>",
814 0,
815 InliningMode::Document,
816 );
817 let mut buffer = Vec::new();
818 doc.serialize(
819 &mut buffer,
820 vec![None; doc.nodes.len()],
821 false,
822 false,
823 false,
824 None,
825 InliningMode::Document,
826 false,
827 false,
828 )
829 .expect("Should not fail");
830 assert_eq!(buffer, b"<html><head></head><body></body></html>");
831 }
832
833 #[test]
834 fn test_escaped() {
835 let doc = Document::parse_with_options(
836 b"<!DOCTYPE html><html><head><title>& < > \xC2\xA0</title></head><body></body></html>",
837 0,
838 InliningMode::Document,
839 );
840 let mut buffer = Vec::new();
841 doc.serialize(
842 &mut buffer,
843 vec![None; doc.nodes.len()],
844 false,
845 false,
846 false,
847 None,
848 InliningMode::Document,
849 false,
850 false,
851 )
852 .expect("Should not fail");
853 assert_eq!(buffer, b"<!DOCTYPE html><html><head><title>& < > </title></head><body></body></html>");
854 }
855
856 #[test]
857 fn test_untouched_style() {
858 let doc = Document::parse_with_options(
859 b"<html><body><p style=\"color:blue;\"></p></body></html>",
860 0,
861 InliningMode::Document,
862 );
863 let mut buffer = Vec::new();
864 doc.serialize(
865 &mut buffer,
866 vec![None; doc.nodes.len()],
867 false,
868 false,
869 false,
870 None,
871 InliningMode::Document,
872 false,
873 false,
874 )
875 .expect("Should not fail");
876 assert_eq!(
877 buffer,
878 b"<html><head></head><body><p style=\"color:blue;\"></p></body></html>"
879 );
880 }
881
882 #[test]
883 fn test_attributes() {
884 let doc = Document::parse_with_options(
885 b"<!DOCTYPE html><html><head></head><body data-foo='& \xC2\xA0 \"'></body></html>",
886 0,
887 InliningMode::Document,
888 );
889 let mut buffer = Vec::new();
890 doc.serialize(
891 &mut buffer,
892 vec![None; doc.nodes.len()],
893 false,
894 false,
895 false,
896 None,
897 InliningMode::Document,
898 false,
899 false,
900 )
901 .expect("Should not fail");
902 assert_eq!(buffer, b"<!DOCTYPE html><html><head></head><body data-foo=\"& "\"></body></html>");
903 }
904
905 #[test]
906 fn test_keep_at_rules_tags() {
907 let doc = Document::parse_with_options(
908 b"<html><head><style>h1 { color:red }</style></head>",
909 0,
910 InliningMode::Document,
911 );
912 let mut buffer = Vec::new();
913 doc.serialize(
914 &mut buffer,
915 vec![None; doc.nodes.len()],
916 false,
917 false,
918 false,
919 Some(&String::from(
920 "@media (max-width: 600px) { h1 { font-size: 18px; } }",
921 )),
922 InliningMode::Document,
923 false,
924 false,
925 )
926 .expect("Should not fail");
927 assert_eq!(buffer, b"<html><head><style>@media (max-width: 600px) { h1 { font-size: 18px; } }</style></head><body></body></html>");
928 }
929}