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