diff --git a/Cargo.lock b/Cargo.lock index 06aaf1e81..07e846226 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -698,11 +698,13 @@ dependencies = [ "futures", "handlebars", "html-escape", + "html5ever", "indexmap 2.11.0", "insta", "itoa", "js-sys", "lazy_static", + "markup5ever_rcdom", "percent-encoding", "pretty_assertions", "regex", @@ -1023,6 +1025,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.31" @@ -1232,6 +1244,20 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -1488,6 +1514,38 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + [[package]] name = "memchr" version = "2.7.5" @@ -1723,6 +1781,16 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + [[package]] name = "phf_generator" version = "0.11.3" @@ -1804,6 +1872,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -2158,6 +2232,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "string_enum" version = "1.0.2" @@ -2493,6 +2592,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2713,6 +2823,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8-width" version = "0.1.7" @@ -3071,6 +3187,17 @@ dependencies = [ "tap", ] +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 6bc2e70a9..feb127603 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,8 @@ futures = "0.3.30" tokio = { version = "1.39.2", features = ["full"] } pretty_assertions = "1.4.0" insta = { version = "1.39.0", features = ["json"] } +html5ever = "0.27.0" +markup5ever_rcdom = "0.3.0" [target.'cfg(target_arch = "wasm32")'.dependencies] url = "2.4.1" diff --git a/src/html/templates/pages/redirect.hbs b/src/html/templates/pages/redirect.hbs index 910e7468b..c48797e12 100644 --- a/src/html/templates/pages/redirect.hbs +++ b/src/html/templates/pages/redirect.hbs @@ -1 +1,11 @@ - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/html_test.rs b/tests/html_test.rs index 4ac3b60fc..e31b7d7bd 100644 --- a/tests/html_test.rs +++ b/tests/html_test.rs @@ -237,7 +237,7 @@ async fn html_doc_files_single() { id_prefix: None, diff_only: false, }, - get_files("single").await, + get_files(std::env::var("PROBE_DS").as_deref().unwrap_or("single")).await, None, ) .unwrap(); @@ -947,3 +947,89 @@ export function hello(): string { ul_depth ); } + +// Parse every generated HTML file with a real HTML5 parser and assert it has +// no parse errors (missing closing tags, mismatched/invalid markup, etc.). +// See issue #634. +fn assert_generated_html_is_valid( + files: &std::collections::HashMap, +) { + use html5ever::parse_document; + use html5ever::tendril::TendrilSink; + use markup5ever_rcdom::RcDom; + + let mut names: Vec<_> = files.keys().collect(); + names.sort(); + + for name in names { + if !name.ends_with(".html") { + continue; + } + let content = &files[name]; + let dom = parse_document(RcDom::default(), Default::default()) + .from_utf8() + .read_from(&mut content.as_bytes()) + .unwrap(); + assert!( + dom.errors.is_empty(), + "generated HTML for {name} is not valid: {:?}", + dom.errors + ); + } +} + +#[tokio::test] +async fn html_output_is_valid() { + // Validate the "multiple" fixture: it exercises the widest range of output + // (classes, interfaces, enums, type aliases, namespaces, drilldown member + // pages, redirects, and the all-symbols/index pages). + let multiple_dir = std::env::current_dir() + .unwrap() + .join("tests") + .join("testdata") + .join("multiple"); + let mut rewrite_map = IndexMap::new(); + rewrite_map.insert( + ModuleSpecifier::from_file_path(multiple_dir.join("a.ts")).unwrap(), + ".".to_string(), + ); + rewrite_map.insert( + ModuleSpecifier::from_file_path(multiple_dir.join("b.ts")).unwrap(), + "foo".to_string(), + ); + rewrite_map.insert( + ModuleSpecifier::from_file_path(multiple_dir.join("c.ts")).unwrap(), + "c".to_string(), + ); + rewrite_map.insert( + ModuleSpecifier::from_file_path(multiple_dir.join("_d.ts")).unwrap(), + "d".to_string(), + ); + + let ctx = GenerateCtx::create_basic( + GenerateOptions { + package_name: None, + main_entrypoint: Some( + ModuleSpecifier::from_file_path(multiple_dir.join("a.ts")).unwrap(), + ), + href_resolver: Arc::new(EmptyResolver), + usage_composer: Some(Arc::new(EmptyResolver)), + rewrite_map: Some(rewrite_map), + category_docs: None, + disable_search: false, + symbol_redirect_map: None, + default_symbol_map: None, + markdown_renderer: comrak::create_renderer(None, None, None), + markdown_stripper: Arc::new(comrak::strip), + head_inject: None, + id_prefix: None, + diff_only: false, + }, + get_files("multiple").await, + None, + ) + .unwrap(); + let files = generate(ctx).unwrap(); + + assert_generated_html_is_valid(&files); +} diff --git a/tests/snapshots/html_test__html_doc_files_multiple-11.snap b/tests/snapshots/html_test__html_doc_files_multiple-11.snap index 797228af8..0e447fbe3 100644 --- a/tests/snapshots/html_test__html_doc_files_multiple-11.snap +++ b/tests/snapshots/html_test__html_doc_files_multiple-11.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/snapshots/html_test__html_doc_files_multiple-13.snap b/tests/snapshots/html_test__html_doc_files_multiple-13.snap index 2c441d4e1..fc84f1dec 100644 --- a/tests/snapshots/html_test__html_doc_files_multiple-13.snap +++ b/tests/snapshots/html_test__html_doc_files_multiple-13.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/snapshots/html_test__html_doc_files_multiple-29.snap b/tests/snapshots/html_test__html_doc_files_multiple-29.snap index 50404116e..9455c344d 100644 --- a/tests/snapshots/html_test__html_doc_files_multiple-29.snap +++ b/tests/snapshots/html_test__html_doc_files_multiple-29.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/snapshots/html_test__html_doc_files_multiple-38.snap b/tests/snapshots/html_test__html_doc_files_multiple-38.snap index 98d0f995f..3e91509a7 100644 --- a/tests/snapshots/html_test__html_doc_files_multiple-38.snap +++ b/tests/snapshots/html_test__html_doc_files_multiple-38.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/snapshots/html_test__html_doc_files_multiple-4.snap b/tests/snapshots/html_test__html_doc_files_multiple-4.snap index 071e34c0e..afc06fae9 100644 --- a/tests/snapshots/html_test__html_doc_files_multiple-4.snap +++ b/tests/snapshots/html_test__html_doc_files_multiple-4.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/snapshots/html_test__html_doc_files_multiple-49.snap b/tests/snapshots/html_test__html_doc_files_multiple-49.snap index 1f543d84d..ee1e3a436 100644 --- a/tests/snapshots/html_test__html_doc_files_multiple-49.snap +++ b/tests/snapshots/html_test__html_doc_files_multiple-49.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/snapshots/html_test__html_doc_files_multiple-8.snap b/tests/snapshots/html_test__html_doc_files_multiple-8.snap index 90d0be177..5e6cd7d0c 100644 --- a/tests/snapshots/html_test__html_doc_files_multiple-8.snap +++ b/tests/snapshots/html_test__html_doc_files_multiple-8.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/snapshots/html_test__html_doc_files_single-4.snap b/tests/snapshots/html_test__html_doc_files_single-4.snap index 2c441d4e1..fc84f1dec 100644 --- a/tests/snapshots/html_test__html_doc_files_single-4.snap +++ b/tests/snapshots/html_test__html_doc_files_single-4.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/snapshots/html_test__html_doc_files_single-6.snap b/tests/snapshots/html_test__html_doc_files_single-6.snap index 50404116e..9455c344d 100644 --- a/tests/snapshots/html_test__html_doc_files_single-6.snap +++ b/tests/snapshots/html_test__html_doc_files_single-6.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + + diff --git a/tests/snapshots/html_test__html_doc_files_single-8.snap b/tests/snapshots/html_test__html_doc_files_single-8.snap index 98d0f995f..3e91509a7 100644 --- a/tests/snapshots/html_test__html_doc_files_single-8.snap +++ b/tests/snapshots/html_test__html_doc_files_single-8.snap @@ -2,4 +2,14 @@ source: tests/html_test.rs expression: files.get(file_name).unwrap() --- - + + + + + + Redirecting… + + + Click here if you are not redirected. + +