diff --git a/.isu/issues.json b/.isu/issues.json index d877918..062441e 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -2861,7 +2861,7 @@ ], "assigned": [], "author": "piefev", - "state": "open", + "state": "closed", "created_at": "2026-05-17T18:30:59Z" }, { diff --git a/crates/e2e/pages/29_script_with_lt.html b/crates/e2e/pages/29_script_with_lt.html new file mode 100644 index 0000000..5dc4f0d --- /dev/null +++ b/crates/e2e/pages/29_script_with_lt.html @@ -0,0 +1,29 @@ + + + + + + +

Inline script with < characters

+
pending
+ + + diff --git a/crates/e2e/scenarios/smoke.we b/crates/e2e/scenarios/smoke.we index 5bdc975..b8a6c1a 100644 --- a/crates/e2e/scenarios/smoke.we +++ b/crates/e2e/scenarios/smoke.we @@ -78,3 +78,10 @@ dump_dom 28_new_member_chain.dom.txt dump_console 28_new_member_chain.console.txt assert_dom_contains "msg=oops; type=bad; arr=9" assert_console_contains "new-member-chain ok" + +goto crates/e2e/pages/29_script_with_lt.html +screenshot 29_script_with_lt.png +dump_dom 29_script_with_lt.dom.txt +dump_console 29_script_with_lt.console.txt +assert_dom_contains "10:20:30:true:0 | 10:20:30:true:1" +assert_console_contains "script-with-lt ok: 6 rows" diff --git a/crates/html/src/lib.rs b/crates/html/src/lib.rs index d0adbae..7c6351e 100644 --- a/crates/html/src/lib.rs +++ b/crates/html/src/lib.rs @@ -46,6 +46,19 @@ pub fn tokenize(input: &str) -> Vec { let mut tokens = Vec::new(); loop { let token = tok.next_token(); + if let Token::StartTag { name, .. } = &token { + // Mirror tree-construction state switching so script/style/title + // content tokenizes correctly when the caller drives the + // tokenizer through this convenience wrapper. + match name.as_str() { + "script" => tok.switch_to_script_data(), + "style" | "iframe" | "xmp" | "noembed" | "noframes" | "noscript" => { + tok.switch_to_rawtext(); + } + "title" | "textarea" => tok.switch_to_rcdata(), + _ => {} + } + } match token { Token::Eof => break, Token::Character(ref s) => { diff --git a/crates/html/src/speculative.rs b/crates/html/src/speculative.rs index d6c57aa..1f87cd4 100644 --- a/crates/html/src/speculative.rs +++ b/crates/html/src/speculative.rs @@ -140,12 +140,25 @@ impl PreloadScanner { // Run the preload scan. let preloads = self.scan(); - // Run the full tokenizer to buffer tokens. + // Run the full tokenizer to buffer tokens. Mirror the tree-builder + // state-switch logic so RCDATA/RAWTEXT/script-data content is + // tokenized correctly (e.g. `<` inside `` matching the appropriate end tag returns to Data state. + ScriptData, + ScriptDataLessThanSign, + ScriptDataEndTagOpen, + ScriptDataEndTagName, + ScriptDataEscapeStart, + ScriptDataEscapeStartDash, + ScriptDataEscaped, + ScriptDataEscapedDash, + ScriptDataEscapedDashDash, + ScriptDataEscapedLessThanSign, + ScriptDataEscapedEndTagOpen, + ScriptDataEscapedEndTagName, + ScriptDataDoubleEscapeStart, + ScriptDataDoubleEscaped, + ScriptDataDoubleEscapedDash, + ScriptDataDoubleEscapedDashDash, + ScriptDataDoubleEscapedLessThanSign, + ScriptDataDoubleEscapeEnd, + // RAWTEXT (§13.2.5.3 et seq.). Used inside "); + assert_eq!( + tokens, + vec![ + Token::StartTag { + name: "style".to_string(), + attributes: vec![], + self_closing: false, + }, + Token::Character(".a{content:'<'}".to_string()), + Token::EndTag { + name: "style".to_string(), + }, + ] + ); + } + + #[test] + fn rcdata_title_keeps_lt_but_resolves_entities() { + let tokens = tokenize("5 < 7 & ok"); + assert_eq!( + tokens, + vec![ + Token::StartTag { + name: "title".to_string(), + attributes: vec![], + self_closing: false, + }, + Token::Character("5 < 7 & ok".to_string()), + Token::EndTag { + name: "title".to_string(), + }, + ] + ); + } } diff --git a/crates/html/src/tree_builder.rs b/crates/html/src/tree_builder.rs index 0091e8d..986ff3f 100644 --- a/crates/html/src/tree_builder.rs +++ b/crates/html/src/tree_builder.rs @@ -1087,7 +1087,18 @@ pub fn parse_html(input: &str) -> Document { loop { let token = tokenizer.next_token(); let is_eof = token == Token::Eof; + // Per HTML5 §13.2.6 the tree construction stage tells the tokenizer + // to switch states for elements whose content uses RCDATA, RAWTEXT, + // or script-data tokenization (so e.g. `<` inside a `"; + let doc = parse_html(src); + // Find the