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