diff --git a/Package.resolved b/Package.resolved index ebe09f39..a2630425 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,13 @@ { "pins" : [ + { + "identity" : "foundationessentialsextras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GoodNotes/FoundationEssentialsExtras.git", + "state" : { + "revision" : "dace29232ec271389cc1e4c4258c8162d1ac3f5f" + } + }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", @@ -8,6 +16,14 @@ "revision" : "41982a3656a71c768319979febd796c6fd111d5c", "version" : "1.5.0" } + }, + { + "identity" : "xylem", + "kind" : "remoteSourceControl", + "location" : "https://github.com/compnerd/xylem.git", + "state" : { + "revision" : "9881c95ce3a139f4ccfa584676201516c2a5751d" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 75d314c4..3c44e2fb 100644 --- a/Package.swift +++ b/Package.swift @@ -2,6 +2,53 @@ import PackageDescription +var dependencies: [Package.Dependency] = [ + .package( + url: "https://github.com/apple/swift-argument-parser.git", + from: "1.5.0" + ), +] +var svgViewDependencies: [Target.Dependency] = [] +var svgViewSwiftSettings: [SwiftSetting] = [] + +#if compiler(>=6.2) +dependencies.append( + .package( + url: "https://github.com/GoodNotes/FoundationEssentialsExtras.git", + revision: "dace29232ec271389cc1e4c4258c8162d1ac3f5f" + ) +) +dependencies.append( + .package( + url: "https://github.com/compnerd/xylem.git", + revision: "9881c95ce3a139f4ccfa584676201516c2a5751d" + ) +) +svgViewDependencies.append(contentsOf: [ + .product( + name: "FoundationEssentialsExtras", + package: "FoundationEssentialsExtras", + condition: .when(platforms: [.wasi]) + ), + .product( + name: "SAXParser", + package: "xylem", + condition: .when(platforms: [.wasi]) + ), + .product( + name: "XMLCore", + package: "xylem", + condition: .when(platforms: [.wasi]) + ), +]) +svgViewSwiftSettings.append( + .define( + "FOUNDATION_ESSENTIALS_BUILD", + .when(platforms: [.wasi]) + ) +) +#endif + let package = Package( name: "SVGView", platforms: [ @@ -19,12 +66,7 @@ let package = Package( targets: ["GenerateReferencesCLI"] ) ], - dependencies: [ - .package( - url: "https://github.com/apple/swift-argument-parser.git", - from: "1.5.0" - ), - ], + dependencies: dependencies, targets: [ .executableTarget( name: "GenerateReferencesCLI", @@ -34,9 +76,11 @@ let package = Package( ], path: "GenerateReferencesCLI" ), - .target( - name: "SVGView", - path: "Source" + .target( + name: "SVGView", + dependencies: svgViewDependencies, + path: "Source", + swiftSettings: svgViewSwiftSettings ), .testTarget( name: "CoreGraphicsPolyfillTests", diff --git a/Source/CoreGraphicsPolyfill.swift b/Source/CoreGraphicsPolyfill.swift index 959d3ea8..44dd1e18 100644 --- a/Source/CoreGraphicsPolyfill.swift +++ b/Source/CoreGraphicsPolyfill.swift @@ -5,7 +5,19 @@ // Created by khoi on 10/5/25. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentialsExtras +import WASILibc +#else import Foundation +#endif + +#if FOUNDATION_ESSENTIALS_BUILD +public typealias CGFloat = FoundationEssentialsExtras.CGFloat +public typealias CGPoint = FoundationEssentialsExtras.CGPoint +public typealias CGSize = FoundationEssentialsExtras.CGSize +public typealias CGRect = FoundationEssentialsExtras.CGRect +#endif #if os(WASI) || os(Linux) || os(Android) private let KAPPA: CGFloat = 0.5522847498 // 4 *(sqrt(2) -1)/3 diff --git a/Source/Model/Images/SVGDataImage.swift b/Source/Model/Images/SVGDataImage.swift index 1c45b238..51adc0c0 100644 --- a/Source/Model/Images/SVGDataImage.swift +++ b/Source/Model/Images/SVGDataImage.swift @@ -5,7 +5,9 @@ // Created by Alisa Mylnikova on 10/06/2021. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Images/SVGURLImage.swift b/Source/Model/Images/SVGURLImage.swift index b96885d3..98bf3b73 100644 --- a/Source/Model/Images/SVGURLImage.swift +++ b/Source/Model/Images/SVGURLImage.swift @@ -5,7 +5,9 @@ // Created by Alisa Mylnikova on 22/09/2021. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Nodes/SVGDefs.swift b/Source/Model/Nodes/SVGDefs.swift index 13763cf8..b1b2313c 100644 --- a/Source/Model/Nodes/SVGDefs.swift +++ b/Source/Model/Nodes/SVGDefs.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Nodes/SVGGroup.swift b/Source/Model/Nodes/SVGGroup.swift index cac5959e..c3170331 100644 --- a/Source/Model/Nodes/SVGGroup.swift +++ b/Source/Model/Nodes/SVGGroup.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Nodes/SVGImage.swift b/Source/Model/Nodes/SVGImage.swift index a44e337f..0ba1401f 100644 --- a/Source/Model/Nodes/SVGImage.swift +++ b/Source/Model/Nodes/SVGImage.swift @@ -5,7 +5,9 @@ // Created by Alisa Mylnikova on 03/06/2021. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Nodes/SVGMarker.swift b/Source/Model/Nodes/SVGMarker.swift index 49009a30..1eb9b8b7 100644 --- a/Source/Model/Nodes/SVGMarker.swift +++ b/Source/Model/Nodes/SVGMarker.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift index a6e05db7..5a13d01c 100644 --- a/Source/Model/Nodes/SVGNode.swift +++ b/Source/Model/Nodes/SVGNode.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Nodes/SVGShape.swift b/Source/Model/Nodes/SVGShape.swift index d2b2ae83..c64c06e4 100644 --- a/Source/Model/Nodes/SVGShape.swift +++ b/Source/Model/Nodes/SVGShape.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Nodes/SVGText.swift b/Source/Model/Nodes/SVGText.swift index 3dedbb42..b917289c 100644 --- a/Source/Model/Nodes/SVGText.swift +++ b/Source/Model/Nodes/SVGText.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Nodes/SVGUserSpaceNode.swift b/Source/Model/Nodes/SVGUserSpaceNode.swift index 7392c3bc..f45973cc 100644 --- a/Source/Model/Nodes/SVGUserSpaceNode.swift +++ b/Source/Model/Nodes/SVGUserSpaceNode.swift @@ -5,7 +5,9 @@ // Created by Alisa Mylnikova on 14/10/2020. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Nodes/SVGViewport.swift b/Source/Model/Nodes/SVGViewport.swift index c1f43306..0dfcb101 100644 --- a/Source/Model/Nodes/SVGViewport.swift +++ b/Source/Model/Nodes/SVGViewport.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGColor.swift b/Source/Model/Primitives/SVGColor.swift index 31fe7b0f..ec9c1121 100644 --- a/Source/Model/Primitives/SVGColor.swift +++ b/Source/Model/Primitives/SVGColor.swift @@ -5,7 +5,9 @@ // Created by Yuriy Strot on 19.01.2021. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI @@ -42,24 +44,24 @@ public class SVGColor: SVGPaint { } public convenience init(hex: String) { - let scanner = Scanner(string: hex) - var rgbValue: UInt64 = 0 - scanner.scanHexInt64(&rgbValue) - self.init(Int(rgbValue)) + self.init(Int(SVGStringUtilities.parseHex(hex) ?? 0)) } override func serialize(key: String, serializer: Serializer) { var prefix = "" let transparency = t if transparency != 0 { - prefix = "\(Int(round(Double(255 - transparency) * 100 / 255)))% " + prefix = "\(Int((Double(255 - transparency) * 100 / 255).rounded()))% " } let hex = ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff) if let text = SVGColors.text(of: hex) { serializer.add(key, "\(prefix)\(text)") } else { - serializer.add(key, "\(prefix)#\(String(format: "%02X%02X%02X", r, g, b))") + serializer.add( + key, + "\(prefix)#\(SVGStringUtilities.hexByteString(r))\(SVGStringUtilities.hexByteString(g))\(SVGStringUtilities.hexByteString(b))" + ) } } @@ -137,14 +139,11 @@ extension Color: SerializableAtom { return "\"\(prefix)\(text)\"" } - return "\"\(prefix)#\(String(format: "%02X%02X%02X", r, g, b))\"" + return "\"\(prefix)#\(SVGStringUtilities.hexByteString(r))\(SVGStringUtilities.hexByteString(g))\(SVGStringUtilities.hexByteString(b))\"" } init(hex: String) { - let scanner = Scanner(string: hex) - var rgbValue: UInt64 = 0 - scanner.scanHexInt64(&rgbValue) - self.init(rgbValue: Int(rgbValue)) + self.init(rgbValue: Int(SVGStringUtilities.parseHex(hex) ?? 0)) } init(rgbValue: Int) { diff --git a/Source/Model/Primitives/SVGFont.swift b/Source/Model/Primitives/SVGFont.swift index 1c777cc9..52a7c60d 100644 --- a/Source/Model/Primitives/SVGFont.swift +++ b/Source/Model/Primitives/SVGFont.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGGradient.swift b/Source/Model/Primitives/SVGGradient.swift index 61908cf3..59fca8ae 100644 --- a/Source/Model/Primitives/SVGGradient.swift +++ b/Source/Model/Primitives/SVGGradient.swift @@ -5,7 +5,10 @@ // Created by Yuriy Strot on 22.02.2021. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +import WASILibc +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGLength.swift b/Source/Model/Primitives/SVGLength.swift index ee947ce2..21ff01f9 100644 --- a/Source/Model/Primitives/SVGLength.swift +++ b/Source/Model/Primitives/SVGLength.swift @@ -5,7 +5,9 @@ // Created by Alisa Mylnikova on 13/10/2020. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGPaint.swift b/Source/Model/Primitives/SVGPaint.swift index f4555959..c2a9ca98 100644 --- a/Source/Model/Primitives/SVGPaint.swift +++ b/Source/Model/Primitives/SVGPaint.swift @@ -5,7 +5,9 @@ // Created by Yuriy Strot on 19.01.2021. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGPreserveAspectRatio.swift b/Source/Model/Primitives/SVGPreserveAspectRatio.swift index 33a7cdba..117db0b0 100644 --- a/Source/Model/Primitives/SVGPreserveAspectRatio.swift +++ b/Source/Model/Primitives/SVGPreserveAspectRatio.swift @@ -5,7 +5,9 @@ // Created by Yuriy Strot on 20.01.2021. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGStroke.swift b/Source/Model/Primitives/SVGStroke.swift index 57d7252e..78b77fdd 100644 --- a/Source/Model/Primitives/SVGStroke.swift +++ b/Source/Model/Primitives/SVGStroke.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Shapes/SVGCircle.swift b/Source/Model/Shapes/SVGCircle.swift index 0d6effcb..a6fdd031 100644 --- a/Source/Model/Shapes/SVGCircle.swift +++ b/Source/Model/Shapes/SVGCircle.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Shapes/SVGEllipse.swift b/Source/Model/Shapes/SVGEllipse.swift index 1dde61b8..526062a4 100644 --- a/Source/Model/Shapes/SVGEllipse.swift +++ b/Source/Model/Shapes/SVGEllipse.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Shapes/SVGLine.swift b/Source/Model/Shapes/SVGLine.swift index 49f2c5e5..557b58d4 100644 --- a/Source/Model/Shapes/SVGLine.swift +++ b/Source/Model/Shapes/SVGLine.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Shapes/SVGPath.swift b/Source/Model/Shapes/SVGPath.swift index 52e23a9a..96cbb569 100644 --- a/Source/Model/Shapes/SVGPath.swift +++ b/Source/Model/Shapes/SVGPath.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI @@ -71,4 +73,3 @@ extension MBezierPath { } } #endif - diff --git a/Source/Model/Shapes/SVGPolygon.swift b/Source/Model/Shapes/SVGPolygon.swift index 83ff8020..ad1547be 100644 --- a/Source/Model/Shapes/SVGPolygon.swift +++ b/Source/Model/Shapes/SVGPolygon.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Shapes/SVGPolyline.swift b/Source/Model/Shapes/SVGPolyline.swift index f3220b29..20034df8 100644 --- a/Source/Model/Shapes/SVGPolyline.swift +++ b/Source/Model/Shapes/SVGPolyline.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Model/Shapes/SVGRect.swift b/Source/Model/Shapes/SVGRect.swift index def171d0..148e9970 100644 --- a/Source/Model/Shapes/SVGRect.swift +++ b/Source/Model/Shapes/SVGRect.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Parser/CSS/CSSParser.swift b/Source/Parser/CSS/CSSParser.swift index 236a525f..4224ae4d 100644 --- a/Source/Parser/CSS/CSSParser.swift +++ b/Source/Parser/CSS/CSSParser.swift @@ -19,7 +19,7 @@ class CSSParser { fileprivate var stylesByTag: [String: [String: String]] = [:] func parse(content: String) { - let parts = content.components(separatedBy: .whitespacesAndNewlines).joined().split(separator: "{") + let parts = SVGStringUtilities.removingWhitespace(from: content).split(separator: "{") var separatedParts = [String.SubSequence]() @@ -40,10 +40,10 @@ class CSSParser { currentStyles = [String: String]() } let style = String(bodies[index]) - let styleParts = style.components(separatedBy: ";") + let styleParts = style.split(separator: ";").map(String.init) styleParts.forEach { styleAttribute in if !styleAttribute.isEmpty { - let currentStyle = styleAttribute.components(separatedBy: ":") + let currentStyle = styleAttribute.split(separator: ":", maxSplits: 1).map(String.init) if currentStyle.count == 2 { currentStyles![currentStyle[0]] = currentStyle[1] } diff --git a/Source/Parser/SVG/Attributes/SVGAttribute.swift b/Source/Parser/SVG/Attributes/SVGAttribute.swift index 64e0f0f1..0e4e304f 100644 --- a/Source/Parser/SVG/Attributes/SVGAttribute.swift +++ b/Source/Parser/SVG/Attributes/SVGAttribute.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 29.05.2022. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif class SVGAttribute { diff --git a/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift b/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift index d1399c4e..2f39bb3a 100644 --- a/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift +++ b/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 29.05.2022. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif class SVGFontSizeAttribute: SVGDefaultAttribute { diff --git a/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift b/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift index 2375752b..ad9cc2a7 100644 --- a/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift +++ b/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift @@ -6,7 +6,11 @@ // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif class SVGLengthAttribute: SVGDefaultAttribute { diff --git a/Source/Parser/SVG/Elements/SVGElementParser.swift b/Source/Parser/SVG/Elements/SVGElementParser.swift index 4b1d1b84..1aed798a 100644 --- a/Source/Parser/SVG/Elements/SVGElementParser.swift +++ b/Source/Parser/SVG/Elements/SVGElementParser.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 29.05.2022. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif protocol SVGElementParser { diff --git a/Source/Parser/SVG/Elements/SVGImageParser.swift b/Source/Parser/SVG/Elements/SVGImageParser.swift index f8a1b470..6d3216c9 100644 --- a/Source/Parser/SVG/Elements/SVGImageParser.swift +++ b/Source/Parser/SVG/Elements/SVGImageParser.swift @@ -5,7 +5,9 @@ // Created by Yuri Strot on 29.05.2022. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Parser/SVG/Elements/SVGStructureParsers.swift b/Source/Parser/SVG/Elements/SVGStructureParsers.swift index 1b1bb10b..4ced3f27 100644 --- a/Source/Parser/SVG/Elements/SVGStructureParsers.swift +++ b/Source/Parser/SVG/Elements/SVGStructureParsers.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 29.05.2022. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif class SVGViewportParser: SVGGroupParser { @@ -54,7 +58,12 @@ class SVGGroupParser: SVGBaseElementParser { class SVGUseParser: SVGBaseElementParser { override func doParse(context: SVGNodeContext, delegate: (XMLElement) -> SVGNode?) -> SVGNode? { - guard let useId = context.properties["xlink:href"]?.replacingOccurrences(of: "#", with: ""), + guard let href = context.properties["xlink:href"] else { + return nil + } + + let useId = SVGStringUtilities.removing(from: href, where: { $0 == "#" }) + guard !useId.isEmpty, let def = context.index.element(by: useId), let useNode = delegate(def) else { return nil diff --git a/Source/Parser/SVG/Elements/SVGTextParser.swift b/Source/Parser/SVG/Elements/SVGTextParser.swift index a86cdb86..772c9871 100644 --- a/Source/Parser/SVG/Elements/SVGTextParser.swift +++ b/Source/Parser/SVG/Elements/SVGTextParser.swift @@ -5,7 +5,9 @@ // Created by Yuri Strot on 29.05.2022. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI @@ -24,7 +26,9 @@ class SVGTextParser: SVGBaseElementParser { let transform = CGAffineTransform(translationX: x, y: y) if let textNode = context.element.contents.first as? XMLText { - let trimmed = textNode.text.trimmingCharacters(in: .whitespacesAndNewlines).processingWhitespaces() + let trimmed = SVGStringUtilities.collapsingWhitespace( + in: SVGStringUtilities.trimmed(textNode.text) + ) return SVGText(text: trimmed, font: font, fill: SVGHelper.parseFill(context.styles, context.index), stroke: SVGHelper.parseStroke(context.styles, index: context.index), textAnchor: textAnchor, transform: transform) } return .none @@ -40,16 +44,4 @@ class SVGTextParser: SVGBaseElementParser { } return .leading } - - static var whitespaceRegex = try! NSRegularExpression(pattern: "\\s+", options: NSRegularExpression.Options.caseInsensitive) - -} - -extension String { - - fileprivate func processingWhitespaces() -> String { - let range = NSMakeRange(0, self.count) - let modString = SVGTextParser.whitespaceRegex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: " ") - return modString - } } diff --git a/Source/Parser/SVG/Primitives/SVGLengthParser.swift b/Source/Parser/SVG/Primitives/SVGLengthParser.swift index 0312d3d6..edf78c9a 100644 --- a/Source/Parser/SVG/Primitives/SVGLengthParser.swift +++ b/Source/Parser/SVG/Primitives/SVGLengthParser.swift @@ -5,7 +5,12 @@ // Created by Yuri Strot on 29.05.2022. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +import WASILibc +#else import Foundation +#endif enum SVGLengthAxis { @@ -60,9 +65,12 @@ class SVGLengthParser { return 0 } - let scanner = Scanner(string: string) - guard let value = scanner.scanDouble() else { return nil } - let unit = scanner.scanCharacters(from: .unit)?.lowercased() + guard let parsed = SVGNumberParser.numberAndUnit(from: string) else { + return nil + } + + let value = parsed.value + let unit = parsed.unit?.lowercased() switch unit { case nil, "px": @@ -95,11 +103,3 @@ class SVGLengthParser { } } - -extension CharacterSet { - - static let unit = CharacterSet(charactersIn: "a"..."z") - .union(CharacterSet(charactersIn: "A"..."Z")) - .union(CharacterSet(charactersIn: "%")) - -} diff --git a/Source/Parser/SVG/SVGConstants.swift b/Source/Parser/SVG/SVGConstants.swift index c957a044..8955b6df 100644 --- a/Source/Parser/SVG/SVGConstants.swift +++ b/Source/Parser/SVG/SVGConstants.swift @@ -1,12 +1,3 @@ -// -// SVGConstants.swift -// SVGView -// -// Created by Alisa Mylnikova on 17/07/2020. -// - -import Foundation - open class SVGConstants { static let groupTags = ["svg", "g"] @@ -36,75 +27,3 @@ open class SVGConstants { ] } - -public class SVGParserRegexHelper { - - fileprivate static let transformAttributePattern = "([a-z]+)\\(((\\-?\\d+\\.?\\d*e?\\-?\\d*\\s*,?\\s*)+)\\)" - fileprivate static let transformPattern = "\\-?\\d+\\.?\\d*e?\\-?\\d*" - fileprivate static let textElementPattern = "((?s:.*))<\\/text>" - fileprivate static let maskIdenitifierPattern = "url\\(#((?s:.*))\\)" - fileprivate static let unitsIdenitifierPattern = "([a-zA-Z]+)$" - - fileprivate static var transformMatcher: NSRegularExpression? - fileprivate static var transformAttributeMatcher: NSRegularExpression? - fileprivate static var textElementMatcher: NSRegularExpression? - fileprivate static var maskIdenitifierMatcher: NSRegularExpression? - fileprivate static var unitsMatcher: NSRegularExpression? - - class func getTransformAttributeMatcher() -> NSRegularExpression? { - if self.transformAttributeMatcher == nil { - do { - self.transformAttributeMatcher = try NSRegularExpression(pattern: transformAttributePattern, options: .caseInsensitive) - } catch { - - } - } - return self.transformAttributeMatcher - } - - class func getTransformMatcher() -> NSRegularExpression? { - if self.transformMatcher == nil { - do { - self.transformMatcher = try NSRegularExpression(pattern: transformPattern, options: .caseInsensitive) - } catch { - - } - } - return self.transformMatcher - } - - class func getTextElementMatcher() -> NSRegularExpression? { - if self.textElementMatcher == nil { - do { - self.textElementMatcher = try NSRegularExpression(pattern: textElementPattern, options: .caseInsensitive) - } catch { - - } - } - return self.textElementMatcher - } - - class func getMaskIdenitifierMatcher() -> NSRegularExpression? { - if self.maskIdenitifierMatcher == nil { - do { - self.maskIdenitifierMatcher = try NSRegularExpression(pattern: maskIdenitifierPattern, options: .caseInsensitive) - } catch { - - } - } - return self.maskIdenitifierMatcher - } - - class func getUnitsIdenitifierMatcher() -> NSRegularExpression? { - if unitsMatcher == nil { - do { - unitsMatcher = try NSRegularExpression(pattern: unitsIdenitifierPattern, options: .caseInsensitive) - } catch { - - } - } - return unitsMatcher - } - -} - diff --git a/Source/Parser/SVG/SVGContext.swift b/Source/Parser/SVG/SVGContext.swift index 6438e7ff..409af60a 100644 --- a/Source/Parser/SVG/SVGContext.swift +++ b/Source/Parser/SVG/SVGContext.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 26.05.2022. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif protocol SVGContext { @@ -76,7 +80,13 @@ class SVGNodeContext: SVGContext { func create(for child: XMLElement) -> SVGNodeContext? { var useIds = self.useIds - if child.name == "use", let useId = child.attributes["xlink:href"]?.replacingOccurrences(of: "#", with: "") { + if child.name == "use", + let href = child.attributes["xlink:href"] { + let useId = SVGStringUtilities.removing(from: href, where: { $0 == "#" }) + guard !useId.isEmpty else { + return nil + } + if useIds.contains(useId) { log(message: " recursion detected!") return nil diff --git a/Source/Parser/SVG/SVGIndex.swift b/Source/Parser/SVG/SVGIndex.swift index 3b9cef75..459bb757 100644 --- a/Source/Parser/SVG/SVGIndex.swift +++ b/Source/Parser/SVG/SVGIndex.swift @@ -5,7 +5,9 @@ // Created by Yuriy Strot on 21.02.2021. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI @@ -66,10 +68,12 @@ class SVGIndex { } private func getParentGradient(_ element: XMLElement) -> SVGGradient? { - if let link = element.attributes["xlink:href"]?.replacingOccurrences(of: " ", with: ""), link.hasPrefix("#") { - - let id = link.replacingOccurrences(of: "#", with: "") - return paints[id] as? SVGGradient + if let rawLink = element.attributes["xlink:href"] { + let link = SVGStringUtilities.removingWhitespace(from: rawLink) + if link.hasPrefix("#") { + let id = String(link.dropFirst()) + return paints[id] as? SVGGradient + } } return nil } @@ -199,7 +203,7 @@ class SVGIndex { if !attributeValue.contains("%") { return SVGHelper.parseCGFloat(element.attributes, attribute, defaultValue: defaultValue) } else { - let value = attributeValue.replacingOccurrences(of: "%", with: "") + let value = SVGStringUtilities.removing(from: attributeValue) { $0 == "%" } if let doubleValue = Double(value) { return CGFloat(doubleValue / 100) } diff --git a/Source/Parser/SVG/SVGParser.swift b/Source/Parser/SVG/SVGParser.swift index 1c11de2e..e934f9a2 100644 --- a/Source/Parser/SVG/SVGParser.swift +++ b/Source/Parser/SVG/SVGParser.swift @@ -5,7 +5,11 @@ // Created by Alisa Mylnikova on 20/07/2020. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif public struct SVGParser { @@ -24,10 +28,12 @@ public struct SVGParser { return parse(xml: xml, settings: settings) } + #if !FOUNDATION_ESSENTIALS_BUILD static public func parse(stream: InputStream, settings: SVGSettings = .default) -> SVGNode? { let xml = DOMParser.parse(stream: stream, logger: settings.logger) return parse(xml: xml, settings: settings) } + #endif static public func parse(xml: XMLElement?, settings: SVGSettings = .default) -> SVGNode? { guard let xml = xml else { return nil } @@ -84,9 +90,12 @@ public struct SVGParser { } if let cssStyle = xml.attributes["style"] { - let styleParts = cssStyle.replacingOccurrences(of: " ", with: "").components(separatedBy: ";") + let styleParts = SVGStringUtilities.split( + SVGStringUtilities.removingWhitespace(from: cssStyle), + where: { $0 == ";" } + ) styleParts.forEach { styleAttribute in - let currentStyle = styleAttribute.components(separatedBy: ":") + let currentStyle = SVGStringUtilities.split(styleAttribute, where: { $0 == ":" }) if currentStyle.count == 2 { styleDict.updateValue(currentStyle[1], forKey: currentStyle[0]) } diff --git a/Source/Parser/SVG/SVGParserBasics.swift b/Source/Parser/SVG/SVGParserBasics.swift index 72901e81..ab6ac89b 100644 --- a/Source/Parser/SVG/SVGParserBasics.swift +++ b/Source/Parser/SVG/SVGParserBasics.swift @@ -5,7 +5,9 @@ // Created by Alisa Mylnikova on 17/07/2020. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI @@ -37,29 +39,21 @@ extension SVGHelper { return 0 } - let scanner = Scanner(string: string) - let value = scanner.scanDouble() - let unit = scanner.scanCharacters(from: .unitCharacters) + guard let parsed = SVGNumberParser.numberAndUnit(from: string) else { + return nil + } - switch unit { + switch parsed.unit { case nil, "px": - return value + return parsed.value default: - print("SVG parsing error. Unit \"\(unit ?? "")\" is not supported") - return value + print("SVG parsing error. Unit \"\(parsed.unit ?? "")\" is not supported") + return parsed.value } } static func parsePointsArray(_ string: String) -> [CGPoint] { - var numbers: [Double] = [] - - let scanner = Scanner(string: string) - while !scanner.isAtEnd { - if let value = scanner.scanDouble() { - numbers.append(value) - } - _ = scanner.scanCharacters(from: [","]) - } + let numbers = SVGNumberParser.numbers(in: string) var points: [CGPoint] = [] var i = 0 @@ -104,7 +98,7 @@ extension SVGHelper { } static func parseColor(_ string: String, _ style: [String: String]) -> SVGColor? { - let normalized = string.replacingOccurrences(of: " ", with: "") + let normalized = SVGStringUtilities.removingWhitespace(from: string) if normalized == "none" || normalized == "transparent" { return .none } else if normalized == "currentColor", let currentColor = style["color"] { @@ -121,7 +115,7 @@ extension SVGHelper { static func createColorFromHex(_ hexString: String) -> SVGColor { var cleanedHexString = hexString if hexString.hasPrefix("#") { - cleanedHexString = hexString.replacingOccurrences(of: "#", with: "") + cleanedHexString = String(hexString.dropFirst()) } if cleanedHexString.count == 3 { let x = Array(cleanedHexString) @@ -134,11 +128,12 @@ extension SVGHelper { let fromIndex = colorString.hasPrefix("rgba") ? 5 : 4 let from = colorString.index(colorString.startIndex, offsetBy: fromIndex) let inPercentage = colorString.contains("%") - let sp = String(colorString.suffix(from: from)) - .replacingOccurrences(of: "%", with: "") - .replacingOccurrences(of: ")", with: "") - .replacingOccurrences(of: " ", with: "") - let x = sp.components(separatedBy: ",") + let sp = SVGStringUtilities.removing( + from: String(colorString.suffix(from: from)) + ) { character in + character == "%" || character == ")" || character.isWhitespace + } + let x = sp.split(separator: ",").map(String.init) var red = 0.0 var green = 0.0 var blue = 0.0 @@ -161,32 +156,15 @@ extension SVGHelper { alpha *= 0.01 } } - return SVGColor(r: Int(round(red)), g: Int(round(green)), b: Int(round(blue))).opacity(min(max(alpha, 0.0), 1.0)) + return SVGColor( + r: Int(red.rounded()), + g: Int(green.rounded()), + b: Int(blue.rounded()) + ).opacity(min(max(alpha, 0.0), 1.0)) } static private func parseIdFromUrl(_ urlString: String) -> String? { - if urlString.hasPrefix("url") { - return urlString.substringWithOffset(fromStart: 5, fromEnd: 1) - } - return .none + SVGStringUtilities.stripURLIdentifier(urlString) } } - -fileprivate extension String { - func substringWithOffset(fromStart: Int, fromEnd: Int) -> String { - let start = index(startIndex, offsetBy: fromStart) - let end = index(endIndex, offsetBy: -fromEnd) - return String(self[start.. Value? { get { - if let k = keys.first(where: { $0.caseInsensitiveCompare(key) == .orderedSame }) { + if let k = keys.first(where: { $0.lowercased() == key.lowercased() }) { return self[k] } return nil diff --git a/Source/Parser/SVG/SVGParserPrimitives.swift b/Source/Parser/SVG/SVGParserPrimitives.swift index 891ec4e9..f5f37c6e 100644 --- a/Source/Parser/SVG/SVGParserPrimitives.swift +++ b/Source/Parser/SVG/SVGParserPrimitives.swift @@ -5,20 +5,22 @@ // Created by Alisa Mylnikova on 20/07/2020. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +import WASILibc +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI #endif -public class SVGHelper: NSObject { +public enum SVGHelper { static func parseUse(_ use: String?) -> String? { - guard let use = use else { - return .none + guard let use else { + return nil } - return use.replacingOccurrences(of: "url(#", with: "") - .replacingOccurrences(of: ")", with: "") + return SVGStringUtilities.stripURLIdentifier(use) ?? use } static func parseId(_ dict: [String: String]) -> String? { @@ -29,8 +31,7 @@ public class SVGHelper: NSObject { guard let markerId = dict[key] else { return .none } - return markerId.replacingOccurrences(of: "url(#", with: "") - .replacingOccurrences(of: ")", with: "") + return SVGStringUtilities.stripURLIdentifier(markerId) } static func parseStroke(_ style: [String: String], index: SVGIndex) -> SVGStroke? { @@ -51,7 +52,9 @@ public class SVGHelper: NSObject { static func getStrokeDashes(_ style: [String: String]) -> [CGFloat] { var dashes = [CGFloat]() if let strokeDashes = style["stroke-dasharray"] { - let separatedValues = strokeDashes.components(separatedBy: CharacterSet(charactersIn: " ,")) + let separatedValues = SVGStringUtilities.split(strokeDashes) { + $0.isWhitespace || $0 == "," + } separatedValues.forEach { value in if let doubleValue = doubleFromString(value) { dashes.append(CGFloat(doubleValue)) @@ -90,93 +93,76 @@ public class SVGHelper: NSObject { } static func parseTransform(_ attributes: String, transform: CGAffineTransform = CGAffineTransform.identity) -> CGAffineTransform { - guard let matcher = SVGParserRegexHelper.getTransformAttributeMatcher() else { + let operations = SVGTransformParser.operations(in: attributes) + guard !operations.isEmpty else { return transform } - let attributes = attributes.replacingOccurrences(of: "\n", with: "") var finalTransform = transform - let fullRange = NSRange(location: 0, length: attributes.count) - - guard let matchedAttribute = matcher.firstMatch(in: attributes, options: .reportCompletion, range: fullRange) else { - return finalTransform - } - let attributeName = (attributes as NSString).substring(with: matchedAttribute.range(at: 1)) - let values = parseTransformValues((attributes as NSString).substring(with: matchedAttribute.range(at: 2))) - if values.isEmpty { - return finalTransform - } - switch attributeName { - case "translate": - if let x = values[0].cgFloatValue { - var y: CGFloat = 0 - if values.indices.contains(1) { - y = values[1].cgFloatValue ?? 0 + for operation in operations { + func value(at index: Int) -> CGFloat? { + guard operation.values.indices.contains(index) else { + return nil } - finalTransform = finalTransform.translatedBy(x: x, y: y) + return CGFloat(operation.values[index]) } - case "scale": - if let x = values[0].cgFloatValue { - var y = x - if values.indices.contains(1) { - y = values[1].cgFloatValue ?? x + + switch operation.name { + case "translate": + if let x = value(at: 0) { + let y = value(at: 1) ?? 0 + finalTransform = finalTransform.translatedBy(x: x, y: y) } - finalTransform = finalTransform.scaledBy(x: x, y: y) - } - case "rotate": - if let angle = values[0].cgFloatValue { - if values.count == 1 { - finalTransform = finalTransform.rotated(by: angle.degreesToRadians) - } else if values.count == 3 { - if let x = values[1].cgFloatValue, let y = values[2].cgFloatValue { - finalTransform = finalTransform.translatedBy(x: x, y: y).rotated(by: angle.degreesToRadians).translatedBy(x: -x, y: -y) + case "scale": + if let x = value(at: 0) { + let y = value(at: 1) ?? x + finalTransform = finalTransform.scaledBy(x: x, y: y) + } + case "rotate": + if let angle = value(at: 0) { + if operation.values.count == 1 { + finalTransform = finalTransform.rotated(by: angle.degreesToRadians) + } else if operation.values.count == 3, let x = value(at: 1), let y = value(at: 2) { + finalTransform = finalTransform + .translatedBy(x: x, y: y) + .rotated(by: angle.degreesToRadians) + .translatedBy(x: 0 - x, y: 0 - y) } } - } - case "skewX": - if let x = values[0].cgFloatValue { - let v = tan((x * .pi) / 180.0) - finalTransform = finalTransform.shear(shx: v, shy: 0) - } - case "skewY": - if let y = values[0].cgFloatValue { - let y = tan((y * .pi) / 180.0) - finalTransform = finalTransform.shear(shx: 0, shy: y) - } - case "matrix": - if values.count != 6 { - return finalTransform - } - if let a = values[0].cgFloatValue, let b = values[1].cgFloatValue, - let c = values[2].cgFloatValue, let d = values[3].cgFloatValue, - let tx = values[4].cgFloatValue, let ty = values[5].cgFloatValue { - - let transformMatrix = CGAffineTransform(a: a, b: b, c: c, d: d, tx: tx, ty: ty) + case "skewX": + if let x = value(at: 0) { + let v = tan((x * .pi) / 180.0) + finalTransform = finalTransform.shear(shx: v, shy: 0) + } + case "skewY": + if let y = value(at: 0) { + let v = tan((y * .pi) / 180.0) + finalTransform = finalTransform.shear(shx: 0, shy: v) + } + case "matrix": + guard operation.values.count == 6, + let a = value(at: 0), + let b = value(at: 1), + let c = value(at: 2), + let d = value(at: 3), + let tx = value(at: 4), + let ty = value(at: 5) else { + continue + } + let transformMatrix = CGAffineTransform( + a: a, + b: b, + c: c, + d: d, + tx: tx, + ty: ty + ) finalTransform = finalTransform.concatenating(transformMatrix) + default: + break } - default: - break - } - let rangeToRemove = NSRange(location: 0, - length: matchedAttribute.range.location + matchedAttribute.range.length) - let newAttributeString = (attributes as NSString).replacingCharacters(in: rangeToRemove, with: "") - return parseTransform(newAttributeString, transform: finalTransform) - } - - static func parseTransformValues(_ values: String, collectedValues: [String] = []) -> [String] { - guard let matcher = SVGParserRegexHelper.getTransformMatcher() else { - return collectedValues - } - var updatedValues: [String] = collectedValues - let fullRange = NSRange(location: 0, length: values.count) - if let matchedValue = matcher.firstMatch(in: values, options: .reportCompletion, range: fullRange) { - let value = (values as NSString).substring(with: matchedValue.range) - updatedValues.append(value) - let rangeToRemove = NSRange(location: 0, length: matchedValue.range.location + matchedValue.range.length) - let newValues = (values as NSString).replacingCharacters(in: rangeToRemove, with: "") - return parseTransformValues(newValues, collectedValues: updatedValues) } - return updatedValues + return finalTransform } static func transformForNodeInRespectiveCoords(respective: SVGNode, absolute: SVGNode) -> CGAffineTransform { @@ -202,13 +188,9 @@ public class SVGHelper: NSObject { static func parseViewBox(_ attributes: [String: String], context: SVGContext) -> CGRect? { // TODO: temporary solution, all attributes should be case insensitive if let string = attributes[ignoreCase: "viewBox"] { - let nums = string.components(separatedBy: .whitespaces) - if nums.count == 4, - let x = SVGLengthParser.xAxis.double(string: nums[0], context: context), - let y = SVGLengthParser.yAxis.double(string: nums[1], context: context), - let width = SVGLengthParser.xAxis.double(string: nums[2], context: context), - let height = SVGLengthParser.yAxis.double(string: nums[3], context: context) { - return CGRect(x: x, y: y, width: width, height: height) + let nums = SVGNumberParser.numbers(in: string) + if nums.count == 4 { + return CGRect(x: nums[0], y: nums[1], width: nums[2], height: nums[3]) } } return nil @@ -229,7 +211,7 @@ public class SVGHelper: NSObject { static func parsePreserveAspectRatio(string: String?, context: SVGContext, defaultValue: SVGPreserveAspectRatio) -> SVGPreserveAspectRatio { if let contentModeString = string { - let strings = contentModeString.components(separatedBy: CharacterSet(charactersIn: " ")) + let strings = SVGStringUtilities.split(contentModeString, where: \.isWhitespace) if strings.count == 1 { // none return SVGPreserveAspectRatio(scaling: parseScaling(strings[0])) } diff --git a/Source/Parser/SVG/SVGPathReader.swift b/Source/Parser/SVG/SVGPathReader.swift index 569bb0af..a61f1ff5 100644 --- a/Source/Parser/SVG/SVGPathReader.swift +++ b/Source/Parser/SVG/SVGPathReader.swift @@ -11,6 +11,9 @@ public typealias MBezierPath = NSBezierPath #elseif os(iOS) || os(tvOS) || os(watchOS) import UIKit public typealias MBezierPath = UIBezierPath +#elseif FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +import WASILibc #elseif os(WASI) || os(Linux) || os(Android) import Foundation #endif diff --git a/Source/Parser/SVG/SVGStringUtilities.swift b/Source/Parser/SVG/SVGStringUtilities.swift new file mode 100644 index 00000000..8d60dbe1 --- /dev/null +++ b/Source/Parser/SVG/SVGStringUtilities.swift @@ -0,0 +1,228 @@ +enum SVGStringUtilities { + + static func trimmed(_ string: String) -> String { + guard + let start = string.firstIndex(where: { !$0.isWhitespace }), + let end = string.lastIndex(where: { !$0.isWhitespace }) + else { + return "" + } + + return String(string[start...end]) + } + + static func removingWhitespace(from string: String) -> String { + String(string.filter { !$0.isWhitespace }) + } + + static func removing( + from string: String, + where shouldRemove: (Character) -> Bool + ) -> String { + String(string.filter { !shouldRemove($0) }) + } + + static func collapsingWhitespace(in string: String) -> String { + var result = String() + result.reserveCapacity(string.count) + + var shouldInsertSpace = false + for character in string { + if character.isWhitespace { + shouldInsertSpace = !result.isEmpty + continue + } + + if shouldInsertSpace { + result.append(" ") + shouldInsertSpace = false + } + + result.append(character) + } + + return result + } + + static func split( + _ string: String, + where isSeparator: (Character) -> Bool + ) -> [String] { + string.split(omittingEmptySubsequences: true, whereSeparator: isSeparator) + .map(String.init) + } + + static func parseHex(_ string: String) -> UInt64? { + UInt64(string, radix: 16) + } + + static func stripURLIdentifier(_ string: String) -> String? { + guard string.hasPrefix("url(#"), string.hasSuffix(")") else { + return nil + } + return String(string.dropFirst(5).dropLast()) + } + + static func hexByteString(_ value: Int) -> String { + let normalized = String(value & 0xff, radix: 16, uppercase: true) + return normalized.count == 1 ? "0\(normalized)" : normalized + } +} + +enum SVGNumberParser { + + static func numberAndUnit(from string: String) -> (value: Double, unit: String?)? { + let trimmed = SVGStringUtilities.trimmed(string) + guard let parsed = parseLeadingNumber(in: trimmed[...]) else { + return nil + } + + let unit = SVGStringUtilities.trimmed(String(trimmed[parsed.endIndex...])) + return (parsed.value, unit.isEmpty ? nil : unit) + } + + static func numbers(in string: String) -> [Double] { + var numbers = [Double]() + var index = string.startIndex + + while index < string.endIndex { + let character = string[index] + if character.isWhitespace || character == "," { + index = string.index(after: index) + continue + } + + if let parsed = parseLeadingNumber(in: string[index...]) { + numbers.append(parsed.value) + index = parsed.endIndex + } else { + index = string.index(after: index) + } + } + + return numbers + } + + private static func parseLeadingNumber( + in string: Substring + ) -> (value: Double, endIndex: String.Index)? { + guard !string.isEmpty else { + return nil + } + + let endIndex = string.endIndex + var index = string.startIndex + + if index < endIndex, string[index] == "+" || string[index] == "-" { + index = string.index(after: index) + } + + let integerStart = index + while index < endIndex, string[index].isWholeNumber { + index = string.index(after: index) + } + let hasIntegerDigits = index != integerStart + + if index < endIndex, string[index] == "." { + index = string.index(after: index) + + let fractionStart = index + while index < endIndex, string[index].isWholeNumber { + index = string.index(after: index) + } + + if !hasIntegerDigits, index == fractionStart { + return nil + } + } else if !hasIntegerDigits { + return nil + } + + let exponentStart = index + if index < endIndex, string[index] == "e" || string[index] == "E" { + index = string.index(after: index) + + if index < endIndex, string[index] == "+" || string[index] == "-" { + index = string.index(after: index) + } + + let exponentDigitsStart = index + while index < endIndex, string[index].isWholeNumber { + index = string.index(after: index) + } + + if exponentDigitsStart == index { + index = exponentStart + } + } + + let numberString = String(string[.. [Operation] { + let cleaned = String(string.filter { $0 != "\n" && $0 != "\r" }) + var operations = [Operation]() + var index = cleaned.startIndex + + while index < cleaned.endIndex { + while index < cleaned.endIndex, cleaned[index].isWhitespace { + index = cleaned.index(after: index) + } + + let nameStart = index + while index < cleaned.endIndex, isASCIIAlpha(cleaned[index]) { + index = cleaned.index(after: index) + } + + guard nameStart != index else { + index = cleaned.index(after: index) + continue + } + + while index < cleaned.endIndex, cleaned[index].isWhitespace { + index = cleaned.index(after: index) + } + + guard index < cleaned.endIndex, cleaned[index] == "(" else { + continue + } + + let name = String(cleaned[nameStart.. Bool { + guard let scalar = character.unicodeScalars.first, character.unicodeScalars.count == 1 else { + return false + } + return (65...90).contains(scalar.value) || (97...122).contains(scalar.value) + } +} diff --git a/Source/Parser/SVG/SVGView.swift b/Source/Parser/SVG/SVGView.swift index 9ad7806d..bf294017 100644 --- a/Source/Parser/SVG/SVGView.swift +++ b/Source/Parser/SVG/SVGView.swift @@ -5,7 +5,9 @@ // Created by Alisa Mylnikova on 20/08/2020. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI @@ -33,9 +35,11 @@ public struct SVGView: View { self.svg = SVGParser.parse(string: string) } + #if !FOUNDATION_ESSENTIALS_BUILD public init(stream: InputStream) { self.svg = SVGParser.parse(stream: stream) } + #endif public init(xml: XMLElement) { self.svg = SVGParser.parse(xml: xml) diff --git a/Source/Parser/SVG/Settings/SVGLinker.swift b/Source/Parser/SVG/Settings/SVGLinker.swift index 2c15bfb4..c6fc3334 100644 --- a/Source/Parser/SVG/Settings/SVGLinker.swift +++ b/Source/Parser/SVG/Settings/SVGLinker.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 27.05.2022. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif public class SVGLinker { diff --git a/Source/Parser/SVG/Settings/SVGLogger.swift b/Source/Parser/SVG/Settings/SVGLogger.swift index 5993755d..85aec5e9 100644 --- a/Source/Parser/SVG/Settings/SVGLogger.swift +++ b/Source/Parser/SVG/Settings/SVGLogger.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 26.05.2022. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif public class SVGLogger { @@ -16,7 +20,11 @@ public class SVGLogger { } public func log(error: Error) { + #if FOUNDATION_ESSENTIALS_BUILD + log(message: String(describing: error)) + #else log(message: error.localizedDescription) + #endif } } diff --git a/Source/Parser/SVG/Settings/SVGSettings.swift b/Source/Parser/SVG/Settings/SVGSettings.swift index f838526b..8a0ad86e 100644 --- a/Source/Parser/SVG/Settings/SVGSettings.swift +++ b/Source/Parser/SVG/Settings/SVGSettings.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 29.05.2022. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif public struct SVGSettings { diff --git a/Source/Parser/XML/DOMParser.swift b/Source/Parser/XML/DOMParser.swift index 90e50442..68738c2e 100644 --- a/Source/Parser/XML/DOMParser.swift +++ b/Source/Parser/XML/DOMParser.swift @@ -5,46 +5,166 @@ // Created by Alisa Mylnikova on 20/08/2020. // -#if os(WASI) || os(Linux) || os(Android) -import Foundation -import FoundationXML +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +import SAXParser +import XMLCore #else import Foundation +#if os(WASI) || os(Linux) || os(Android) +import FoundationXML +#endif #endif public struct DOMParser { static public func parse(contentsOf url: URL, logger: SVGLogger = .console) -> XMLElement? { - parse(XMLParser(contentsOf: url), logger: logger) + #if FOUNDATION_ESSENTIALS_BUILD + guard let data = try? Data(contentsOf: url) else { + return nil + } + return parse(bytes: Array(data), logger: logger) + #else + return parse(XMLParser(contentsOf: url), logger: logger) + #endif } @available(*, deprecated, message: "Use parse(contentsOf:) function instead") static public func parse(fileURL: URL, logger: SVGLogger = .console) -> XMLElement? { - parse(XMLParser(contentsOf: fileURL), logger: logger) + parse(contentsOf: fileURL, logger: logger) } static public func parse(data: Data, logger: SVGLogger = .console) -> XMLElement? { - parse(XMLParser(data: data), logger: logger) + #if FOUNDATION_ESSENTIALS_BUILD + return parse(bytes: Array(data), logger: logger) + #else + return parse(XMLParser(data: data), logger: logger) + #endif } static public func parse(string: String?, using encoding: String.Encoding = .utf8, logger: SVGLogger = .console) -> XMLElement? { - guard let data = string?.data(using: encoding) else { return nil } + guard let string else { + return nil + } + + #if FOUNDATION_ESSENTIALS_BUILD + return parse(bytes: Array(string.utf8), logger: logger) + #else + guard let data = string.data(using: encoding) else { + return nil + } return parse(XMLParser(data: data), logger: logger) + #endif } + #if !FOUNDATION_ESSENTIALS_BUILD static public func parse(stream: InputStream, logger: SVGLogger = .console) -> XMLElement? { parse(XMLParser(stream: stream), logger: logger) } - + #endif + + #if FOUNDATION_ESSENTIALS_BUILD + static private func parse(bytes: [UInt8], logger: SVGLogger) -> XMLElement? { + var parser = SAXParser(handler: XylemXMLDelegate()) + + do { + try parser.parse(bytes: bytes.span) + return parser.handler.root + } catch { + logger.log(error: error) + return nil + } + } + #else static private func parse(_ parser: XMLParser?, logger: SVGLogger) -> XMLElement? { - let delegate = XMLDelegate(logger: logger) + let delegate = FoundationXMLDelegate(logger: logger) parser?.delegate = delegate parser?.parse() return delegate.root } + #endif } -class XMLDelegate: NSObject, XMLParserDelegate { +#if FOUNDATION_ESSENTIALS_BUILD +private struct XylemXMLDelegate: Handler { + + typealias Failure = XML.Error + + var location: XML.Location? + + var root: XMLElement? + var stack = [XMLElement]() + + mutating func start( + element name: XML.QualifiedNameView, + namespace uri: Span?, + attributes: XML.ResolvedAttributes + ) { + let element = XMLElement( + name: decode(name.bytes), + attributes: decode(attributes: attributes) + ) + stack.last?.contents.append(element) + stack.append(element) + if root == nil { + root = element + } + } + + mutating func end(element name: XML.QualifiedNameView, namespace uri: Span?) { + _ = stack.popLast() + } + + mutating func characters(_ data: Span) { + guard let element = stack.last else { + return + } + + let string = decode(data) + guard !string.isEmpty else { + return + } + + if let textNode = element.contents.last as? XMLText { + textNode.text.append(string) + } else { + element.contents.append(XMLText(text: string)) + } + } + + mutating func character(data: Span) { + characters(data) + } + + mutating func comment(_ content: Span) {} + mutating func declaration(version: Span, encoding: Span?, standalone: Span?) {} + mutating func start(document: Void) {} + mutating func end(document: Void) {} + mutating func processing(target: Span, data: Span?) {} + mutating func start(mapping prefix: Span?, uri: Span) {} + mutating func end(mapping prefix: Span?) {} + mutating func start(dtd name: Span, id: (public: Span?, system: Span?)) {} + mutating func end(dtd: Void) {} + + private func decode(_ bytes: Span) -> String { + bytes.withUnsafeBufferPointer { + String(decoding: $0, as: UTF8.self) + } + } + + private func decode(attributes: XML.ResolvedAttributes) -> [String: String] { + var result = [String: String]() + result.reserveCapacity(attributes.count) + + for index in attributes.indices { + result[decode(attributes.name(at: index).bytes)] = decode(attributes.value(at: index)) + } + + return result + } +} +#else +private class FoundationXMLDelegate: NSObject, XMLParserDelegate { let logger: SVGLogger var root: XMLElement? @@ -80,5 +200,5 @@ class XMLDelegate: NSObject, XMLParserDelegate { func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { logger.log(error: parseError) } - } +#endif diff --git a/Source/Parser/XML/XMLNode.swift b/Source/Parser/XML/XMLNode.swift index c16d8c60..ec1a6cf3 100644 --- a/Source/Parser/XML/XMLNode.swift +++ b/Source/Parser/XML/XMLNode.swift @@ -1,4 +1,6 @@ -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Source/Serialization/Serializable.swift b/Source/Serialization/Serializable.swift index 57ca0f8e..6a40c33e 100644 --- a/Source/Serialization/Serializable.swift +++ b/Source/Serialization/Serializable.swift @@ -5,7 +5,11 @@ // Created by Yuriy Strot on 18.01.2021. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif protocol SerializableAtom { diff --git a/Source/Serialization/Serializations.swift b/Source/Serialization/Serializations.swift index 1945190e..4c50f950 100644 --- a/Source/Serialization/Serializations.swift +++ b/Source/Serialization/Serializations.swift @@ -5,7 +5,9 @@ // Created by Yuriy Strot on 18.01.2021. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI @@ -22,7 +24,18 @@ extension Bool: SerializableAtom { extension String: SerializableAtom { func serialize() -> String { - return "\"\(self.replacingOccurrences(of: "\"", with: "\\\""))\"" + var escaped = String() + escaped.reserveCapacity(count) + + for character in self { + if character == "\"" { + escaped.append("\\\"") + } else { + escaped.append(character) + } + } + + return "\"\(escaped)\"" } } @@ -36,6 +49,7 @@ extension CGFloat: SerializableAtom { } +#if !FOUNDATION_ESSENTIALS_BUILD extension Double: SerializableAtom { func serialize() -> String { @@ -43,23 +57,60 @@ extension Double: SerializableAtom { } } +#endif + +private enum SVGPortableDecimalFormatter { + private static let maxFractionDigits = 10 + private static let scale: Double = 10_000_000_000.0 + private static let scaleInteger: UInt64 = 10_000_000_000 + + static func string(for value: Double) -> String { + let rounded = (value * scale).rounded() / scale + if rounded == 0 { + return "0" + } + if !rounded.isFinite { + return rounded.description + } + + let sign = rounded < 0 ? "-" : "" + let absoluteValue = Swift.abs(rounded) + if absoluteValue >= Double(UInt64.max) { + let fallback = rounded.description + return fallback.hasSuffix(".0") + ? String(fallback.dropLast(2)) + : fallback + } + var integerPart = UInt64(absoluteValue.rounded(.towardZero)) + var fractionalPart = UInt64(((absoluteValue - Double(integerPart)) * scale).rounded()) + + if fractionalPart == scaleInteger { + integerPart += 1 + fractionalPart = 0 + } + + if fractionalPart == 0 { + return "\(sign)\(integerPart)" + } + + var fraction = String(fractionalPart) + if fraction.count < maxFractionDigits { + fraction = String(repeating: "0", count: maxFractionDigits - fraction.count) + fraction + } + + while fraction.last == "0" { + fraction.removeLast() + } + + return "\(sign)\(integerPart).\(fraction)" + } +} extension CGAffineTransform: SerializableAtom { func serialize() -> String { - let formatter = NumberFormatter() - formatter.decimalSeparator = "." - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 10 - let nums = [a, b, c, d, tx, ty] - - var result = "" - for num in nums { - result += formatter.string(for: num) ?? "n/a" - result += ", " - } - return "[\(result.dropLast(2))]" + return "[\(nums.map { SVGPortableDecimalFormatter.string(for: Double($0)) }.joined(separator: ", "))]" } } diff --git a/Source/Serialization/Serializer.swift b/Source/Serialization/Serializer.swift index e9732c42..66c6469e 100644 --- a/Source/Serialization/Serializer.swift +++ b/Source/Serialization/Serializer.swift @@ -5,7 +5,11 @@ // Created by Yuriy Strot on 17.01.2021. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif class Serializer { diff --git a/Source/UI/MBezierPath+Extension_macOS.swift b/Source/UI/MBezierPath+Extension_macOS.swift index 4828cdfb..9017652f 100644 --- a/Source/UI/MBezierPath+Extension_macOS.swift +++ b/Source/UI/MBezierPath+Extension_macOS.swift @@ -6,7 +6,11 @@ // Copyright © 2017 Exyte. All rights reserved. // +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#else import Foundation +#endif #if os(OSX) import AppKit diff --git a/Source/UI/UIExtensions.swift b/Source/UI/UIExtensions.swift index 1975293c..3090adfa 100644 --- a/Source/UI/UIExtensions.swift +++ b/Source/UI/UIExtensions.swift @@ -5,7 +5,9 @@ // Created by Yuri Strot on 25.05.2022. // -#if os(WASI) || os(Linux) || os(Android) +#if FOUNDATION_ESSENTIALS_BUILD +import FoundationEssentials +#elseif os(WASI) || os(Linux) || os(Android) import Foundation #else import SwiftUI diff --git a/Tests/SVGViewTests/DOMParserTests.swift b/Tests/SVGViewTests/DOMParserTests.swift new file mode 100644 index 00000000..dca7a7b9 --- /dev/null +++ b/Tests/SVGViewTests/DOMParserTests.swift @@ -0,0 +1,34 @@ +import XCTest +import class SVGView.XMLElement +import class SVGView.XMLText + +@testable import SVGView + +final class DOMParserTests: XCTestCase { + func testParsesSVGTree() throws { + let source = "" + let xml = try XCTUnwrap(DOMParser.parse(string: source)) + + let root = try XCTUnwrap(SVGParser.parse(xml: xml)) + + XCTAssertNotNil(root.getNode(byId: "shape")) + } + + func testParsesAttributesAndTextEntities() throws { + let source = "one & two" + let root = try XCTUnwrap(DOMParser.parse(string: source)) + let textElement = try XCTUnwrap(root.contents.first as? XMLElement) + let text = try XCTUnwrap(textElement.contents.first as? XMLText) + + XCTAssertEqual(textElement.attributes["id"], "label") + XCTAssertEqual(text.text, "one & two") + } + + func testPreservesQualifiedAttributeNames() throws { + let source = "" + let root = try XCTUnwrap(DOMParser.parse(string: source)) + let use = try XCTUnwrap(root.contents.first as? XMLElement) + + XCTAssertEqual(use.attributes["xlink:href"], "#shape") + } +} diff --git a/Tests/SVGViewTests/SVGParsingUtilitiesTests.swift b/Tests/SVGViewTests/SVGParsingUtilitiesTests.swift new file mode 100644 index 00000000..f46987d2 --- /dev/null +++ b/Tests/SVGViewTests/SVGParsingUtilitiesTests.swift @@ -0,0 +1,182 @@ +import Foundation +import XCTest + +@testable import SVGView + +final class SVGParsingUtilitiesTests: XCTestCase { + func testStringUtilitiesMatchFoundationWhitespaceBehavior() { + let values = [ + "", + "plain", + " padded ", + "\tline one\nline two\r\n", + "one two\tthree", + ] + + for value in values { + XCTAssertEqual( + SVGStringUtilities.trimmed(value), + value.trimmingCharacters(in: .whitespacesAndNewlines), + value + ) + XCTAssertEqual( + SVGStringUtilities.removingWhitespace(from: value), + value.components(separatedBy: .whitespacesAndNewlines).joined(), + value + ) + XCTAssertEqual( + SVGStringUtilities.collapsingWhitespace( + in: SVGStringUtilities.trimmed(value) + ), + foundationCollapsingWhitespace( + in: value.trimmingCharacters(in: .whitespacesAndNewlines) + ), + value + ) + } + } + + func testHexUtilitiesMatchFoundationFormatting() { + for value in [0, 1, 15, 16, 127, 255, 256, -1] { + XCTAssertEqual( + SVGStringUtilities.hexByteString(value), + String(format: "%02X", value & 0xff), + "value: \(value)" + ) + } + + for value in ["0", "0f", "FF", "abcdef"] { + let scanner = Scanner(string: value) + var expected: UInt64 = 0 + XCTAssertTrue(scanner.scanHexInt64(&expected), value) + XCTAssertEqual(SVGStringUtilities.parseHex(value), expected, value) + } + } + + func testNumberAndUnitParserMatchesFoundationScanner() { + let values = [ + "0", + "12", + "-3.5px", + "+.5%", + "1e3", + "-2.5e-2em", + " 42 px ", + "5.", + "1e", + "invalid", + ] + + for value in values { + let expected = foundationNumberAndUnit(from: value) + let actual = SVGNumberParser.numberAndUnit(from: value) + + XCTAssertEqual(actual?.value, expected?.value, value) + XCTAssertEqual(actual?.unit, expected?.unit, value) + } + } + + func testNumberListParserMatchesFoundationScanner() { + let values = [ + "0 1", + "0,1 2,3", + "-1.5e2,+.25 5. 6e-2", + " 10, 20, 30, 40 ", + ] + + for value in values { + XCTAssertEqual( + SVGNumberParser.numbers(in: value), + foundationNumbers(in: value), + value + ) + } + } + + func testTransformParserMatchesFoundationRegex() throws { + let values = [ + "translate(10, 20)", + "scale(2) rotate(-45)", + "matrix(1 0 0 1 10 -20)", + "translate(1e2, -2.5e-1)\nscale(0.5)", + "skewX(12.5) skewY(-8)", + ] + + for value in values { + let expected = try foundationTransformOperations(in: value) + let actual = SVGTransformParser.operations(in: value) + + XCTAssertEqual(actual.count, expected.count, value) + for (actualOperation, expectedOperation) in zip(actual, expected) { + XCTAssertEqual(actualOperation.name, expectedOperation.name, value) + XCTAssertEqual(actualOperation.values, expectedOperation.values, value) + } + } + } + + private func foundationCollapsingWhitespace(in value: String) -> String { + let regex = try! NSRegularExpression(pattern: "\\s+") + let range = NSRange(value.startIndex.. (value: Double, unit: String?)? { + let scanner = Scanner(string: value) + guard let number = scanner.scanDouble() else { + return nil + } + + let unitCharacters = CharacterSet.letters.union(CharacterSet(charactersIn: "%")) + return (number, scanner.scanCharacters(from: unitCharacters)) + } + + private func foundationNumbers(in value: String) -> [Double] { + let scanner = Scanner(string: value) + var result = [Double]() + + while !scanner.isAtEnd { + if let number = scanner.scanDouble() { + result.append(number) + } + _ = scanner.scanCharacters(from: CharacterSet(charactersIn: ",")) + } + + return result + } + + private func foundationTransformOperations( + in value: String + ) throws -> [(name: String, values: [Double])] { + let attributeRegex = try NSRegularExpression( + pattern: "([a-z]+)\\(((\\-?\\d+\\.?\\d*e?\\-?\\d*\\s*,?\\s*)+)\\)", + options: .caseInsensitive + ) + let numberRegex = try NSRegularExpression( + pattern: "\\-?\\d+\\.?\\d*e?\\-?\\d*", + options: .caseInsensitive + ) + + var remaining = value.replacingOccurrences(of: "\n", with: "") + var result = [(name: String, values: [Double])]() + + while let match = attributeRegex.firstMatch( + in: remaining, + range: NSRange(remaining.startIndex..