#![allow(clippy::needless_update)] use std::{ mem::take, path::{Path, PathBuf}, }; use swc_common::{FileName, Span}; use swc_html_ast::*; use swc_html_codegen::{ writer::basic::{BasicHtmlWriter, BasicHtmlWriterConfig, IndentType, LineFeed}, CodeGenerator, CodegenConfig, Emit, }; use swc_html_parser::{ parse_file_as_document, parse_file_as_document_fragment, parser::ParserConfig, }; use swc_html_visit::{VisitMut, VisitMutWith}; use testing::{assert_eq, run_test2, NormalizedOutput}; fn print_document( input: &Path, parser_config: Option, writer_config: Option, codegen_config: Option, ) { let dir = input.parent().unwrap(); let parser_config = match parser_config { Some(parser_config) => parser_config, _ => ParserConfig::default(), }; let writer_config = match writer_config { Some(writer_config) => writer_config, _ => BasicHtmlWriterConfig::default(), }; let codegen_config = match codegen_config { Some(codegen_config) => codegen_config, _ => CodegenConfig::default(), }; let output = if codegen_config.minify { dir.join(format!( "output.min.{}", input.extension().unwrap().to_string_lossy() )) } else { dir.join(format!( "output.{}", input.extension().unwrap().to_string_lossy() )) }; run_test2(false, |cm, handler| { let fm = cm.load_file(input).unwrap(); let mut errors = vec![]; let mut document: Document = parse_file_as_document(&fm, parser_config, &mut errors).unwrap(); for err in take(&mut errors) { err.to_diagnostics(&handler).emit(); } let mut html_str = String::new(); let wr = BasicHtmlWriter::new(&mut html_str, None, writer_config); let mut gen = CodeGenerator::new(wr, codegen_config); gen.emit(&document).unwrap(); let fm_output = cm.load_file(&output).unwrap(); NormalizedOutput::from(html_str) .compare_to_file(output) .unwrap(); let mut errors = vec![]; let mut document_parsed_again = parse_file_as_document(&fm_output, parser_config, &mut errors).map_err(|err| { err.to_diagnostics(&handler).emit(); })?; for error in take(&mut errors) { error.to_diagnostics(&handler).emit(); } document.visit_mut_with(&mut NormalizeTest); document_parsed_again.visit_mut_with(&mut NormalizeTest); assert_eq!(document, document_parsed_again); Ok(()) }) .unwrap(); } fn print_document_fragment( input: &Path, context_element: Element, parser_config: Option, writer_config: Option, codegen_config: Option, ) { let dir = input.parent().unwrap(); let parser_config = match parser_config { Some(parser_config) => parser_config, _ => ParserConfig::default(), }; let writer_config = match writer_config { Some(writer_config) => writer_config, _ => BasicHtmlWriterConfig::default(), }; let codegen_config = match codegen_config { Some(codegen_config) => codegen_config, _ => CodegenConfig::default(), }; let output = if codegen_config.minify { dir.join(format!( "output.min.{}", input.extension().unwrap().to_string_lossy() )) } else { dir.join(format!( "output.{}", input.extension().unwrap().to_string_lossy() )) }; run_test2(false, |cm, handler| { let fm = cm.load_file(input).unwrap(); let mut errors = vec![]; let mut document_fragment = parse_file_as_document_fragment( &fm, context_element.clone(), parser_config, &mut errors, ) .unwrap(); for err in take(&mut errors) { err.to_diagnostics(&handler).emit(); } let mut html_str = String::new(); let wr = BasicHtmlWriter::new(&mut html_str, None, writer_config); let mut gen = CodeGenerator::new(wr, codegen_config); gen.emit(&document_fragment).unwrap(); let fm_output = cm.load_file(&output).unwrap(); NormalizedOutput::from(html_str) .compare_to_file(output) .unwrap(); let mut errors = vec![]; let mut document_fragment_parsed_again = parse_file_as_document_fragment( &fm_output, context_element.clone(), parser_config, &mut errors, ) .map_err(|err| { err.to_diagnostics(&handler).emit(); })?; for error in take(&mut errors) { error.to_diagnostics(&handler).emit(); } document_fragment.visit_mut_with(&mut NormalizeTest); document_fragment_parsed_again.visit_mut_with(&mut NormalizeTest); assert_eq!(document_fragment, document_fragment_parsed_again); Ok(()) }) .unwrap(); } fn verify_document( input: &Path, parser_config: Option, writer_config: Option, codegen_config: Option, ignore_errors: bool, ) { let parser_config = match parser_config { Some(parser_config) => parser_config, _ => ParserConfig::default(), }; let writer_config = match writer_config { Some(writer_config) => writer_config, _ => BasicHtmlWriterConfig::default(), }; let codegen_config = match codegen_config { Some(codegen_config) => codegen_config, _ => CodegenConfig::default(), }; testing::run_test2(false, |cm, handler| { let fm = cm.load_file(input).unwrap(); let mut errors = vec![]; let mut document = parse_file_as_document(&fm, parser_config, &mut errors).map_err(|err| { err.to_diagnostics(&handler).emit(); })?; if !ignore_errors { for err in take(&mut errors) { err.to_diagnostics(&handler).emit(); } } let mut html_str = String::new(); let wr = BasicHtmlWriter::new(&mut html_str, None, writer_config); let mut gen = CodeGenerator::new(wr, codegen_config); gen.emit(&document).unwrap(); let new_fm = cm.new_source_file(FileName::Anon, html_str); let mut parsed_errors = vec![]; let mut document_parsed_again = parse_file_as_document(&new_fm, parser_config, &mut parsed_errors).map_err(|err| { err.to_diagnostics(&handler).emit(); })?; if !ignore_errors { for err in parsed_errors { err.to_diagnostics(&handler).emit(); } } document.visit_mut_with(&mut DropSpan); document_parsed_again.visit_mut_with(&mut DropSpan); assert_eq!(document, document_parsed_again); Ok(()) }) .unwrap(); } fn verify_document_fragment( input: &Path, context_element: Element, parser_config: Option, writer_config: Option, codegen_config: Option, ignore_errors: bool, ) { let parser_config = match parser_config { Some(parser_config) => parser_config, _ => ParserConfig::default(), }; let writer_config = match writer_config { Some(writer_config) => writer_config, _ => BasicHtmlWriterConfig::default(), }; let codegen_config = match codegen_config { Some(codegen_config) => codegen_config, _ => CodegenConfig::default(), }; testing::run_test2(false, |cm, handler| { let fm = cm.load_file(input).unwrap(); let mut errors = vec![]; let mut document_fragment = parse_file_as_document_fragment( &fm, context_element.clone(), parser_config, &mut errors, ) .map_err(|err| { err.to_diagnostics(&handler).emit(); })?; if !ignore_errors { for err in take(&mut errors) { err.to_diagnostics(&handler).emit(); } } let mut html_str = String::new(); let wr = BasicHtmlWriter::new(&mut html_str, None, writer_config); let mut gen = CodeGenerator::new(wr, codegen_config); gen.emit(&document_fragment).unwrap(); let new_fm = cm.new_source_file(FileName::Anon, html_str); let mut parsed_errors = vec![]; let mut document_fragment_parsed_again = parse_file_as_document_fragment( &new_fm, context_element.clone(), parser_config, &mut parsed_errors, ) .map_err(|err| { err.to_diagnostics(&handler).emit(); })?; if !ignore_errors { for err in parsed_errors { err.to_diagnostics(&handler).emit(); } } document_fragment.visit_mut_with(&mut DropSpan); document_fragment_parsed_again.visit_mut_with(&mut DropSpan); assert_eq!(document_fragment, document_fragment_parsed_again); Ok(()) }) .unwrap(); } struct DropSpan; impl VisitMut for DropSpan { fn visit_mut_span(&mut self, n: &mut Span) { *n = Default::default() } } struct NormalizeTest; impl VisitMut for NormalizeTest { fn visit_mut_document_fragment(&mut self, n: &mut DocumentFragment) { n.visit_mut_children_with(self); if let Some(last) = n.children.last_mut() { if let Child::Text(_) = last { // Drop value from the last `Text` node because characters after `` // moved to body tag n.children.remove(n.children.len() - 1); } } } fn visit_mut_element(&mut self, n: &mut Element) { n.visit_mut_children_with(self); if &*n.tag_name == "body" { if let Some(last) = n.children.last_mut() { if let Child::Text(text) = last { // Drop value from the last `Text` node because characters after `` // moved to body tag text.value = "".into(); } } } } fn visit_mut_span(&mut self, n: &mut Span) { *n = Default::default() } } #[testing::fixture("tests/fixture/**/input.html")] fn test_document(input: PathBuf) { print_document( &input, None, None, Some(CodegenConfig { scripting_enabled: false, minify: false, }), ); print_document( &input, None, None, Some(CodegenConfig { scripting_enabled: false, minify: true, }), ); } #[testing::fixture("tests/document_fragment/**/input.html")] fn test_document_fragment(input: PathBuf) { print_document_fragment( &input, Element { span: Default::default(), tag_name: "template".into(), namespace: Namespace::HTML, attributes: vec![], children: vec![], content: None, }, None, None, Some(CodegenConfig { scripting_enabled: false, minify: false, }), ); print_document_fragment( &input, Element { span: Default::default(), tag_name: "template".into(), namespace: Namespace::HTML, attributes: vec![], children: vec![], content: None, }, None, None, Some(CodegenConfig { scripting_enabled: false, minify: true, }), ); } #[testing::fixture("tests/options/indent_type/**/input.html")] fn test_indent_type_option(input: PathBuf) { print_document( &input, None, Some(BasicHtmlWriterConfig { indent_type: IndentType::Tab, indent_width: 2, linefeed: LineFeed::default(), }), None, ); } #[testing::fixture("../swc_html_parser/tests/fixture/**/*.html")] fn parser_verify(input: PathBuf) { verify_document(&input, None, None, None, false); } #[testing::fixture( "../swc_html_parser/tests/recovery/**/*.html", exclude( "document_type/bogus/input.html", "document_type/wrong-name/input.html", ) )] fn parser_recovery_verify(input: PathBuf) { verify_document(&input, None, None, None, true); } // TODO - remove exclude when we implement `raw`, `context_element` and etc #[testing::fixture( "../swc_html_parser/tests/html5lib-tests-fixture/**/*.html", exclude( "tests16_dat/131.html", "plain-text-unsafe_dat/24.html", "template_dat/107.html", "template_dat/80.html", "tests16_dat/130.html", "tests16_dat/132.html", "tests16_dat/133.html", "tests16_dat/134.html", "tests16_dat/135.html", "tests16_dat/136.html", "tests16_dat/147.html", "tests16_dat/148.html", "tests16_dat/149.html", "tests16_dat/150.html", "tests16_dat/196.html", "tests16_dat/31.html", "tests16_dat/32.html", "tests16_dat/33.html", "tests16_dat/34.html", "tests16_dat/35.html", "tests16_dat/36.html", "tests16_dat/37.html", "tests16_dat/48.html", "tests16_dat/49.html", "tests16_dat/50.html", "tests16_dat/51.html", "tests16_dat/52.html", "tests16_dat/53.html", "tests18_dat/12.html", "tests18_dat/15.html", "tests18_dat/21.html", "tests18_dat/7.html", "tests18_dat/8.html", "tests18_dat/9.html", "tests19_dat/103.html", "tests1_dat/103.html", "tests1_dat/30.html", "tests1_dat/77.html", "tests1_dat/90.html", "tests20_dat/41.html", "tests26_dat/2.html", "tests2_dat/12.html", "tests4_dat/3.fragment.style.html", "tests4_dat/4.fragment.plaintext.html", ) )] fn html5lib_tests_verify(input: PathBuf) { let file_stem = input.file_stem().unwrap().to_str().unwrap().to_owned(); let scripting_enabled = file_stem.contains("script_on"); let parser_config = ParserConfig { scripting_enabled }; let codegen_config = CodegenConfig { minify: false, scripting_enabled, }; if file_stem.contains("fragment") { let mut context_element_namespace = Namespace::HTML; let mut context_element_tag_name = ""; let context_element = file_stem .split('.') .last() .expect("failed to get context element from filename"); if context_element.contains('_') { let mut splited = context_element.split('_'); if let Some(namespace) = splited.next() { context_element_namespace = match namespace { "math" => Namespace::MATHML, "svg" => Namespace::SVG, _ => { unreachable!(); } }; } if let Some(tag_name) = splited.next() { context_element_tag_name = tag_name; } } else { context_element_tag_name = context_element; } let context_element = Element { span: Default::default(), namespace: context_element_namespace, tag_name: context_element_tag_name.into(), attributes: vec![], children: vec![], content: None, }; verify_document_fragment( &input, context_element, Some(parser_config), None, Some(codegen_config), true, ); } else { verify_document( &input, Some(parser_config), None, Some(codegen_config), true, ); } }