diff --git a/DOM/Sources/Parser.XML.Scanner.swift b/DOM/Sources/Parser.XML.Scanner.swift index bc057cc..0bebd02 100644 --- a/DOM/Sources/Parser.XML.Scanner.swift +++ b/DOM/Sources/Parser.XML.Scanner.swift @@ -55,7 +55,14 @@ package extension XMLParser { package mutating func scanStringIfPossible(_ token: String) -> Bool { return (try? self.scanString(token)) == true } - + + @discardableResult + package mutating func nextScanString(_ token: String) -> Bool { + scanner.currentIndex = currentIndex + defer { scanner.currentIndex = currentIndex } + return scanStringIfPossible(token) + } + package mutating func scanString(matchingAny tokens: Set) throws -> String { scanner.currentIndex = currentIndex guard let match = tokens.first(where: { scanner.scanString($0) != nil }) else { diff --git a/DOM/Sources/Parser.XML.StyleSheet.swift b/DOM/Sources/Parser.XML.StyleSheet.swift index d0756b9..1a2b58e 100644 --- a/DOM/Sources/Parser.XML.StyleSheet.swift +++ b/DOM/Sources/Parser.XML.StyleSheet.swift @@ -71,13 +71,15 @@ extension XMLParser { var scanner = XMLParser.Scanner(text: removeCSSComments(from: text)) var entries = [DOM.StyleSheet.Selector: [String: String]]() - var last: (DOM.StyleSheet.Selector, [String: String])? - repeat { - last = try scanner.scanNextSelector() - if let last = last { - entries[last.0] = last.1 + while let (selectors, attributes) = try scanner.scanNextSelectorDecl() { + for selector in selectors { + var copy = entries[selector] ?? [:] + for (key, value) in attributes { + copy[key] = value + } + entries[selector] = copy } - } while last != nil + } return entries } @@ -91,15 +93,10 @@ extension XMLParser { extension XMLParser.Scanner { - mutating func scanNextSelector() throws -> (DOM.StyleSheet.Selector, [String: String])? { - if let c = try scanNextClass() { - return (.class(c), try scanAtttributes()) - } else if let id = try scanNextID() { - return (.id(id), try scanAtttributes()) - } else if let e = try scanNextElement() { - return (.element(e), try scanAtttributes()) - } - return nil + mutating func scanNextSelectorDecl() throws -> ([DOM.StyleSheet.Selector], [String: String])? { + let selectorTypes = try scanSelectorTypes() + guard !selectorTypes.isEmpty else { return nil } + return (selectorTypes, try scanAtttributes()) } private mutating func scanNextClass() throws -> String? { @@ -124,7 +121,30 @@ extension XMLParser.Scanner { } private mutating func scanSelectorName() throws -> String? { - try scanString(upTo: "{").trimmingCharacters(in: .whitespacesAndNewlines) + guard !nextScanString("{") else { return nil } + let name = try scanString(upTo: .init(charactersIn: "{,")).trimmingCharacters(in: .whitespacesAndNewlines) + scanStringIfPossible(",") + return name + } + + mutating func scanSelectorTypes() throws -> [DOM.StyleSheet.Selector] { + var selectors: [DOM.StyleSheet.Selector] = [] + while let next = try scanNextSelectorType() { + selectors.append(next) + } + return selectors + } + + private mutating func scanNextSelectorType() throws -> DOM.StyleSheet.Selector? { + if let name = try scanNextClass() { + return .class(name) + } else if let name = try scanNextID() { + return .id(name) + } else if let name = try scanNextElement() { + return .element(name) + } else { + return nil + } } private mutating func scanAtttributes() throws -> [String: String] { diff --git a/DOM/Tests/Parser.XML.StyleSheetTests.swift b/DOM/Tests/Parser.XML.StyleSheetTests.swift index 31582d0..86bee8e 100644 --- a/DOM/Tests/Parser.XML.StyleSheetTests.swift +++ b/DOM/Tests/Parser.XML.StyleSheetTests.swift @@ -110,4 +110,43 @@ final class ParserXMLStyleSheetTests: XCTestCase { XCTAssertEqual(sheet[.class("b")]?.fill, .color(.keyword(.blue))) XCTAssertEqual(sheet[.element("rect")]?.fill, .color(.keyword(.pink))) } + + func testMergesSelectors() throws { + let entries = try XMLParser.parseEntries( + """ + .a { + fill: red; + } + .a { + stroke: blue; + } + .a { + fill: purple; + } + """ + ) + + XCTAssertEqual( + entries, + [.class("a"): ["fill": "purple", "stroke": "blue"]] + ) + } + + func testMutlipleSelectors() throws { + let entries = try XMLParser.parseEntries( + """ + .a, .b { + fill: red; + } + """ + ) + + XCTAssertEqual( + entries, + [ + .class("a"): ["fill": "red"], + .class("b"): ["fill": "red"] + ] + ) + } } diff --git a/Examples/Sources/GalleryView.swift b/Examples/Sources/GalleryView.swift index 69d9e61..fb24148 100644 --- a/Examples/Sources/GalleryView.swift +++ b/Examples/Sources/GalleryView.swift @@ -52,7 +52,8 @@ struct GalleryView: View { "yawning.svg", "thats-no-moon.svg", "alert.svg", - "effigy.svg" + "effigy.svg", + "stylesheet-multiple.svg" ].compactMap { SVG(named: $0, in: .samples) } diff --git a/Samples.bundle/stylesheet-multiple.svg b/Samples.bundle/stylesheet-multiple.svg new file mode 100644 index 0000000..9fb95ae --- /dev/null +++ b/Samples.bundle/stylesheet-multiple.svg @@ -0,0 +1 @@ + \ No newline at end of file