diff --git a/DOM/Sources/DOM.swift b/DOM/Sources/DOM.swift index 7fa2118a..c13da254 100644 --- a/DOM/Sources/DOM.swift +++ b/DOM/Sources/DOM.swift @@ -121,6 +121,8 @@ extension DOM { case millimeter case point case pica + case em + case ex } package enum Error: Swift.Error { @@ -143,6 +145,10 @@ package extension DOM.Unit { return "pt" case .pica: return "pc" + case .em: + return "em" + case .ex: + return "ex" } } } @@ -162,6 +168,8 @@ package extension Double { return self * 1.3333 case .pica: return self * 16 + case .em, .ex: + return self } } } diff --git a/DOM/Sources/Parser.XML.SVG.swift b/DOM/Sources/Parser.XML.SVG.swift index c0f2ec4e..910beb70 100644 --- a/DOM/Sources/Parser.XML.SVG.swift +++ b/DOM/Sources/Parser.XML.SVG.swift @@ -88,15 +88,26 @@ package extension XMLParser { func resolveRootDimension(_ raw: String?, viewport: DOM.Coordinate?, attribute: String) throws -> DOM.Coordinate? { guard let raw, !raw.isEmpty else { return nil } var scanner = XMLParser.Scanner(text: raw) - let value = try scanner.scanCoordinate() + scanner.scanner.currentIndex = scanner.currentIndex + guard let number = scanner.scanner.scanDoubleCompatibly() else { + throw Error.invalidAttribute(name: attribute, value: raw) + } + scanner.currentIndex = scanner.scanner.currentIndex + if scanner.scanStringIfPossible("%") { guard let viewport else { return nil } - return value / 100 * viewport + return DOM.Coordinate(number / 100) * viewport + } + if let unit = scanner.scanUnit() { + guard scanner.isEOF else { + throw Error.invalidAttribute(name: attribute, value: raw) + } + return DOM.Coordinate(number.apply(unit: unit)) } guard scanner.isEOF else { throw Error.invalidAttribute(name: attribute, value: raw) } - return value + return DOM.Coordinate(number) } func makeUnresolvedReason(attribute: String, raw: String?, hasViewBox: Bool) -> String { diff --git a/DOM/Sources/Parser.XML.Scanner.swift b/DOM/Sources/Parser.XML.Scanner.swift index ad4e3ba9..2e0aa7f4 100644 --- a/DOM/Sources/Parser.XML.Scanner.swift +++ b/DOM/Sources/Parser.XML.Scanner.swift @@ -35,7 +35,7 @@ package extension XMLParser { struct Scanner { - private let scanner: Foundation.Scanner + package let scanner: Foundation.Scanner package var currentIndex: String.Index package init(text: String) { @@ -256,7 +256,7 @@ package extension XMLParser { package mutating func scanDouble() throws -> Double { scanner.currentIndex = currentIndex - guard let val = scanner.scanDouble() else { + guard let val = scanner.scanDoubleCompatibly() else { throw Error.invalid } currentIndex = scanner.currentIndex @@ -285,6 +285,10 @@ package extension XMLParser { return .point } else if scanUnit(.pica) { return .pica + } else if scanUnit(.em) { + return .em + } else if scanUnit(.ex) { + return .ex } else { return nil } @@ -404,9 +408,42 @@ package extension Scanner { _ = scanCharacter() return scalar } + + func scanDoubleCompatibly() -> Double? { + #if canImport(Darwin) + return scanDouble() + #else + let start = currentIndex + if let value = scanDouble() { + return value + } + + currentIndex = start + let numeric = Foundation.CharacterSet(charactersIn: "+-0123456789.Ee") + guard let scannedNumber = scanCharacters(from: numeric), !scannedNumber.isEmpty else { + currentIndex = start + return nil + } + + var numericEnd = currentIndex + var numericPrefix = scannedNumber + while Double(numericPrefix) == nil, !numericPrefix.isEmpty { + numericEnd = string.index(before: numericEnd) + numericPrefix.removeLast() + } + + guard let value = Double(numericPrefix), !numericPrefix.isEmpty else { + currentIndex = start + return nil + } + + currentIndex = numericEnd + return value + #endif + } func scanCoordinate() throws -> DOM.Coordinate { - guard let val = scanDouble() else { throw XMLParser.Error.invalid } + guard let val = scanDoubleCompatibly() else { throw XMLParser.Error.invalid } return DOM.Coordinate(val) } diff --git a/DOM/Tests/Parser.SVGTests.swift b/DOM/Tests/Parser.SVGTests.swift index f6b5ed65..0c065739 100644 --- a/DOM/Tests/Parser.SVGTests.swift +++ b/DOM/Tests/Parser.SVGTests.swift @@ -68,6 +68,29 @@ struct ParserSVGTests { #expect(parsed.height == 192) } + @Test + func root_em_ex_dimensions_fall_back_to_raw_values() throws { + let em = XML.Element(name: "svg", attributes: ["width": "2em", "height": "1em"]) + let ex = XML.Element(name: "svg", attributes: ["width": "2ex", "height": "1ex"]) + let parser = DOMXMLParser() + + let parsedEm = try parser.parseSVG(em) + #expect(parsedEm.width == 2) + #expect(parsedEm.height == 1) + + let parsedEx = try parser.parseSVG(ex) + #expect(parsedEx.width == 2) + #expect(parsedEx.height == 1) + } + + @Test + func unknown_root_units_still_throw() { + let node = XML.Element(name: "svg", attributes: ["width": "2abc", "height": "1abc"]) + #expect(throws: XMLParser.Error.self) { + try XMLParser().parseSVG(node) + } + } + @Test func parseSVGInvalidNode() { let node = XML.Element(name: "svg2", attributes: ["width": "100", "height": "200"]) diff --git a/DOM/Tests/Parser.XML.PathTests.swift b/DOM/Tests/Parser.XML.PathTests.swift index 09e68aa5..2740fc1f 100644 --- a/DOM/Tests/Parser.XML.PathTests.swift +++ b/DOM/Tests/Parser.XML.PathTests.swift @@ -62,6 +62,17 @@ struct ParserXMLPathTests { #expect(throws: (any Error).self) { _ = try scanner.scanCoordinate() } } + @Test + func scanCoordinate_FontUnitsFallBackToRawValues() throws { + let emScanner = XMLParser.PathScanner(string: "2em") + let exScanner = XMLParser.PathScanner(string: "3ex") + + #expect(try emScanner.scanCoordinate() == 2.0) + #expect(emScanner.isAtEnd == false) + #expect(try exScanner.scanCoordinate() == 3.0) + #expect(exScanner.isAtEnd == false) + } + @Test func equality() { #expect(Segment.move(x: 10, y: 20, space: .relative) == move(10, 20, .relative)) diff --git a/DOM/Tests/XML.Parser.ScannerTests.swift b/DOM/Tests/XML.Parser.ScannerTests.swift index c291ded3..4d41139e 100644 --- a/DOM/Tests/XML.Parser.ScannerTests.swift +++ b/DOM/Tests/XML.Parser.ScannerTests.swift @@ -134,9 +134,38 @@ struct ScannerTests { #expect(scanDouble("124") == 124) #expect(scanDouble(" 045") == 45) #expect(scanDouble("-29") == -29) + #expect(scanDouble("2em") == 2) + #expect(scanDouble(".5em") == 0.5) #expect(scanDouble("ab24") == nil) } + @Test + func foundationScanDouble_WithTrailingEmEx_IsPlatformDependent() { + let emScanner = Foundation.Scanner(string: "2em") + emScanner.charactersToBeSkipped = Foundation.CharacterSet.whitespacesAndNewlines + + let exScanner = Foundation.Scanner(string: "2ex") + exScanner.charactersToBeSkipped = Foundation.CharacterSet.whitespacesAndNewlines + + #if canImport(Darwin) + #expect(emScanner.scanDouble() == 2) + #expect(exScanner.scanDouble() == 2) + #else + #expect(emScanner.scanDouble() == nil) + #expect(exScanner.scanDouble() == nil) + #endif + } + + @Test + func scanDoubleCompatibly_FallsBackForTrailingFontUnits() { + #expect(scanDoubleCompatibly("2em") == 2) + #expect(scanDoubleCompatibly("2ex") == 2) + #expect(scanDoubleCompatibly("2e") == 2) + #expect(scanDoubleCompatibly("2e+") == 2) + #expect(scanDoubleCompatibly(".5em") == 0.5) + #expect(scanDoubleCompatibly("ab24") == nil) + } + @Test func scan_Length() { #expect(scanLength("0") == 0) @@ -217,6 +246,13 @@ struct ScannerTests { #expect(try scanner.scanCoordinate() == 5 * 16) } + @Test + func scanCoordinate_FontUnitsFallBackToRawValues() { + #expect(parseCoordinate("2em") == 2) + #expect(parseCoordinate("2ex") == 2) + #expect(parseCoordinate(".5em") == 0.5) + } + @Test func scanUpToPreservesStrings() throws { var scanner = XMLParser.Scanner(text: #"foo: bar; baz: "bing;bob" zab;"#) @@ -289,6 +325,17 @@ private func scanDouble(_ text: String) -> Double? { return try? scanner.scanDouble() } +private func scanDoubleCompatibly(_ text: String) -> Double? { + let scanner = Foundation.Scanner(string: text) + scanner.charactersToBeSkipped = Foundation.CharacterSet.whitespacesAndNewlines + return scanner.scanDoubleCompatibly() +} + +private func parseCoordinate(_ text: String) -> DOM.Coordinate? { + var scanner = XMLParser.Scanner(text: text) + return try? scanner.scanCoordinate() +} + private func scanLength(_ text: String) -> DOM.Length? { var scanner = XMLParser.Scanner(text: text) return try? scanner.scanLength() diff --git a/Examples/Sources/GalleryView.swift b/Examples/Sources/GalleryView.swift index 54723de3..bd8885f2 100644 --- a/Examples/Sources/GalleryView.swift +++ b/Examples/Sources/GalleryView.swift @@ -55,6 +55,7 @@ struct GalleryView: View { "alert.svg", "effigy.svg", "d10.svg", + "mathjax-latex.svg", "stylesheet-multiple.svg" ] diff --git a/Samples.bundle/mathjax-latex.svg b/Samples.bundle/mathjax-latex.svg new file mode 100644 index 00000000..6eb063df --- /dev/null +++ b/Samples.bundle/mathjax-latex.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDraw/Tests/SVGTests.swift b/SwiftDraw/Tests/SVGTests.swift index aa6daf96..cc1dea2b 100644 --- a/SwiftDraw/Tests/SVGTests.swift +++ b/SwiftDraw/Tests/SVGTests.swift @@ -59,6 +59,21 @@ struct SVGTests { #expect(SVG(named: "missing.svg", in: .test) == nil) } + @Test + func root_em_ex_dimensions_parse_as_raw_values() { + let ex = #""# + let em = #""# + let px = #""# + + let exSVG = SVG(data: Data(ex.utf8)) + let emSVG = SVG(data: Data(em.utf8)) + let pxSVG = SVG(data: Data(px.utf8)) + + #expect(exSVG?.size == CGSize(width: 2, height: 1)) + #expect(emSVG?.size == CGSize(width: 2, height: 1)) + #expect(pxSVG?.size == CGSize(width: 2, height: 1)) + } + @Test func imageRasterizes() { let image = SVG.makeLines() diff --git a/docker-test.sh b/container-run-tests.sh similarity index 76% rename from docker-test.sh rename to container-run-tests.sh index 9c96c3f5..56cac05a 100755 --- a/docker-test.sh +++ b/container-run-tests.sh @@ -1,5 +1,5 @@ -docker run -it \ +container run -it \ --rm \ --mount src="$(pwd)",target=/SwiftDraw,type=bind \ - swift \ + swift:6.3 \ /usr/bin/swift test --package-path /SwiftDraw \ No newline at end of file