From ed1d5a70ac9f140bb442c3708a6a105881849a73 Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Tue, 24 Mar 2026 10:54:47 +0200 Subject: [PATCH] Fix CSS @media query parsing error in style elements Skip unknown CSS @ rules (e.g. @media, @supports) including nested blocks when parsing style sheets. Previously the parser would fail when encountering @media queries, printing a parsing error to console. The scanner now tracks brace depth to correctly skip over nested rule blocks while continuing to parse surrounding selectors. Closes #59 --- DOM/Sources/Parser.XML.StyleSheet.swift | 22 ++++++++++++ DOM/Tests/Parser.XML.StyleSheetTests.swift | 41 ++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/DOM/Sources/Parser.XML.StyleSheet.swift b/DOM/Sources/Parser.XML.StyleSheet.swift index 91d7fa5..c3ccae0 100644 --- a/DOM/Sources/Parser.XML.StyleSheet.swift +++ b/DOM/Sources/Parser.XML.StyleSheet.swift @@ -126,6 +126,9 @@ extension XMLParser.Scanner { if let attributes = try scanNextFontFace() { return (.atRule("font-face"), attributes) } + if let name = scanUnknownAtRule() { + return (.atRule(name), [:]) + } let selectorTypes = try scanSelectorTypes() guard !selectorTypes.isEmpty else { return nil } return (.selector(selectorTypes), try scanAtttributes()) @@ -148,6 +151,25 @@ extension XMLParser.Scanner { return try scanAtttributes() } + /// Skips unknown @ rules (e.g. @media, @supports) including nested blocks. + /// Returns the rule name if an unknown @ rule was consumed, nil otherwise. + mutating func scanUnknownAtRule() -> String? { + guard doScanString("@") else { return nil } + guard let name = try? scanString(upTo: .init(charactersIn: "{")), + doScanString("{") else { return nil } + var depth = 1 + while depth > 0 && !isEOF { + if doScanString("{") { + depth += 1 + } else if doScanString("}") { + depth -= 1 + } else { + _ = try? scanString(upTo: .init(charactersIn: "{}")) + } + } + return name.trimmingCharacters(in: .whitespacesAndNewlines) + } + private mutating func scanNextElement() throws -> String? { do { return try scanSelectorName() diff --git a/DOM/Tests/Parser.XML.StyleSheetTests.swift b/DOM/Tests/Parser.XML.StyleSheetTests.swift index 9116669..3c60fc5 100644 --- a/DOM/Tests/Parser.XML.StyleSheetTests.swift +++ b/DOM/Tests/Parser.XML.StyleSheetTests.swift @@ -191,6 +191,47 @@ struct ParserXMLStyleSheetTests { ) } + @Test + func skipsMediaQueries() throws { + let entries = try XMLParser.parseSelectorEntries( + """ + g { fill: #000; } + + @media (prefers-color-scheme: dark) { + g { fill: #fff; } + } + """ + ) + + #expect( + entries == [ + .element("g"): ["fill": "#000"] + ] + ) + } + + @Test + func skipsNestedAtRules() throws { + let entries = try XMLParser.parseSelectorEntries( + """ + .a { fill: red; } + @supports (display: grid) { + @media (min-width: 600px) { + .b { fill: blue; } + } + } + .c { stroke: green; } + """ + ) + + #expect( + entries == [ + .class("a"): ["fill": "red"], + .class("c"): ["stroke": "green"] + ] + ) + } + @Test func parsesFontFaceEntries() throws { let entries = try XMLParser.parseFontFaceEntries(