diff --git a/submodules/PhotoResources/BUILD b/submodules/PhotoResources/BUILD
index 31205c31e0..5afacafb6d 100644
--- a/submodules/PhotoResources/BUILD
+++ b/submodules/PhotoResources/BUILD
@@ -24,7 +24,7 @@ swift_library(
"//submodules/WebPBinding:WebPBinding",
"//submodules/AppBundle:AppBundle",
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
- "//submodules/Svg:Svg",
+ "//submodules/Svg/LegacyImpl",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/ImageCompression",
],
diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift
index 8b530c5978..86a270792e 100644
--- a/submodules/PhotoResources/Sources/PhotoResources.swift
+++ b/submodules/PhotoResources/Sources/PhotoResources.swift
@@ -17,10 +17,10 @@ import TinyThumbnail
import ImageTransparency
import AppBundle
import MusicAlbumArtResources
-import Svg
import RangeSet
import Accelerate
import ImageCompression
+import LegacyImpl
private enum ResourceFileData {
case data(Data)
diff --git a/submodules/Svg/BUILD b/submodules/Svg/BUILD
index eba76650f4..b559ec3b70 100644
--- a/submodules/Svg/BUILD
+++ b/submodules/Svg/BUILD
@@ -1,21 +1,16 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
-objc_library(
+swift_library(
name = "Svg",
- enable_modules = True,
module_name = "Svg",
srcs = glob([
- "Sources/**/*.m",
- "Sources/**/*.c",
- "Sources/**/*.h",
+ "Sources/**/*.swift",
]),
- hdrs = glob([
- "PublicHeaders/**/*.h",
- ]),
- includes = [
- "PublicHeaders",
+ copts = [
+ "-warnings-as-errors",
],
- sdk_frameworks = [
- "Foundation",
+ deps = [
+ "//submodules/Svg/LegacyImpl",
],
visibility = [
"//visibility:public",
diff --git a/submodules/Svg/LegacyImpl/BUILD b/submodules/Svg/LegacyImpl/BUILD
new file mode 100644
index 0000000000..6808c17505
--- /dev/null
+++ b/submodules/Svg/LegacyImpl/BUILD
@@ -0,0 +1,23 @@
+
+objc_library(
+ name = "LegacyImpl",
+ enable_modules = True,
+ module_name = "LegacyImpl",
+ srcs = glob([
+ "Sources/**/*.m",
+ "Sources/**/*.c",
+ "Sources/**/*.h",
+ ]),
+ hdrs = glob([
+ "PublicHeaders/**/*.h",
+ ]),
+ includes = [
+ "PublicHeaders",
+ ],
+ sdk_frameworks = [
+ "Foundation",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/Svg/PublicHeaders/Svg/Svg.h b/submodules/Svg/LegacyImpl/PublicHeaders/Svg/Svg.h
similarity index 84%
rename from submodules/Svg/PublicHeaders/Svg/Svg.h
rename to submodules/Svg/LegacyImpl/PublicHeaders/Svg/Svg.h
index 39080185f3..acb4abd69f 100755
--- a/submodules/Svg/PublicHeaders/Svg/Svg.h
+++ b/submodules/Svg/LegacyImpl/PublicHeaders/Svg/Svg.h
@@ -27,6 +27,6 @@ GiftPatternData * _Nullable getGiftPatternData(NSData * _Nonnull data);
UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit);
UIImage * _Nullable renderPreparedImageWithSymbol(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit, UIImage * _Nullable symbolImage, int32_t modelRectIndex);
-UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, CGFloat scale, bool opaque);
+UIImage * _Nullable drawSvgImageImpl(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, CGFloat scale, bool opaque);
#endif /* Lottie_h */
diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/LegacyImpl/Sources/Svg.m
similarity index 99%
rename from submodules/Svg/Sources/Svg.m
rename to submodules/Svg/LegacyImpl/Sources/Svg.m
index d3c4edee41..a7c73827b1 100755
--- a/submodules/Svg/Sources/Svg.m
+++ b/submodules/Svg/LegacyImpl/Sources/Svg.m
@@ -302,7 +302,7 @@ void renderShape(NSVGshape *shape, CGContextRef context, UIColor *foregroundColo
}
}
-UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor, CGFloat canvasScale, bool opaque) {
+UIImage * _Nullable drawSvgImageImpl(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor, CGFloat canvasScale, bool opaque) {
if (!data || data.length == 0) return nil;
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
diff --git a/submodules/Svg/Sources/nanosvg.c b/submodules/Svg/LegacyImpl/Sources/nanosvg.c
similarity index 100%
rename from submodules/Svg/Sources/nanosvg.c
rename to submodules/Svg/LegacyImpl/Sources/nanosvg.c
diff --git a/submodules/Svg/Sources/nanosvg.h b/submodules/Svg/LegacyImpl/Sources/nanosvg.h
similarity index 100%
rename from submodules/Svg/Sources/nanosvg.h
rename to submodules/Svg/LegacyImpl/Sources/nanosvg.h
diff --git a/submodules/Svg/Sources/Svg.swift b/submodules/Svg/Sources/Svg.swift
new file mode 100644
index 0000000000..3d70dbddd7
--- /dev/null
+++ b/submodules/Svg/Sources/Svg.swift
@@ -0,0 +1,7 @@
+import Foundation
+import UIKit
+import LegacyImpl
+
+public func drawSvgImage(data: Data, size: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor?, scale: CGFloat, opaque: Bool) -> UIImage? {
+ return drawSvgImageImpl(data, size, backgroundColor, foregroundColor, scale, opaque)
+}
diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD
index 764a33bcce..a33742767d 100644
--- a/submodules/TelegramUI/BUILD
+++ b/submodules/TelegramUI/BUILD
@@ -219,7 +219,7 @@ swift_library(
"//submodules/ItemListVenueItem:ItemListVenueItem",
"//submodules/SemanticStatusNode:SemanticStatusNode",
"//submodules/AccountUtils:AccountUtils",
- "//submodules/Svg:Svg",
+ "//submodules/Svg/LegacyImpl",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TooltipUI:TooltipUI",
"//submodules/ListMessageItem:ListMessageItem",
diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift
index ba7530fe28..01da82422c 100644
--- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift
+++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift
@@ -343,7 +343,7 @@ public final class PeerInfoRatingComponent: Component {
}
if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_outer", withExtension: "svg"), let data = try? Data(contentsOf: url) {
- if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.borderColor) {
+ if let image = generateTintedImage(image: drawSvgImage(data: data, size: size, backgroundColor: nil, foregroundColor: nil, scale: 0.0, opaque: false), color: component.borderColor) {
image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0)
}
}
@@ -372,7 +372,7 @@ public final class PeerInfoRatingComponent: Component {
}
if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_inner", withExtension: "svg"), let data = try? Data(contentsOf: url) {
- if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.backgroundColor) {
+ if let image = generateTintedImage(image: drawSvgImage(data: data, size: size, backgroundColor: nil, foregroundColor: nil, scale: 0.0, opaque: false), color: component.backgroundColor) {
image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0)
}
}
diff --git a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift
index 08ad7c9271..0e6be30087 100644
--- a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift
+++ b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift
@@ -18,7 +18,7 @@ import WallpaperResources
import GZip
import TelegramUniversalVideoContent
import GradientBackground
-import Svg
+import LegacyImpl
import UniversalMediaPlayer
import RangeSet
diff --git a/submodules/WallpaperResources/BUILD b/submodules/WallpaperResources/BUILD
index 8fcb8fa8c2..999d61068c 100644
--- a/submodules/WallpaperResources/BUILD
+++ b/submodules/WallpaperResources/BUILD
@@ -21,7 +21,7 @@ swift_library(
"//submodules/PhotoResources:PhotoResources",
"//submodules/PersistentStringHash:PersistentStringHash",
"//submodules/AppBundle:AppBundle",
- "//submodules/Svg:Svg",
+ "//submodules/Svg/LegacyImpl",
"//submodules/GZip:GZip",
"//submodules/GradientBackground:GradientBackground",
"//submodules/TelegramPresentationData:TelegramPresentationData",
diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift
index 588c1cc851..5c33034f8f 100644
--- a/submodules/WallpaperResources/Sources/WallpaperResources.swift
+++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift
@@ -14,7 +14,7 @@ import LocalMediaResources
import TelegramPresentationData
import TelegramUIPreferences
import AppBundle
-import Svg
+import LegacyImpl
import GradientBackground
import GZip
@@ -1360,7 +1360,7 @@ public func themeImage(account: Account, accountManager: AccountManager Point {
+ Point(x: value, y: y)
+ }
+
+ /// Create a new instance maintaining the `x` value while using the provided `y` value.
+ public func y(_ value: Double) -> Point {
+ Point(x: x, y: value)
+ }
+
+ /// The _mirror_ of the instance, rotated 180° around the provided `point`
+ /// in a two-dimensional plane.
+ ///
+ /// - parameters:
+ /// - point: The anchor to use in calculating the reflection.
+ public func reflecting(around point: Point) -> Point {
+ let x = (2 * point.x) - x
+ let y = (2 * point.y) - y
+ return Point(x: x, y: y)
+ }
+
+ public static func == (lhs: Point, rhs: Point) -> Bool {
+ if lhs.x.isNaN, rhs.x.isNaN, lhs.y.isNaN, rhs.y.isNaN {
+ return true
+ }
+
+ if lhs.x.isInfinite, rhs.x.isInfinite, lhs.y.isInfinite, rhs.y.isInfinite {
+ return true
+ }
+
+ return lhs.x == rhs.x && lhs.y == rhs.y
+ }
+}
+
+public extension Point {
+ @available(*, deprecated, renamed: "x(_:)")
+ func with(x value: Double) -> Point {
+ x(value)
+ }
+
+ @available(*, deprecated, renamed: "y(_:)")
+ func with(y value: Double) -> Point {
+ y(value)
+ }
+
+ @available(*, deprecated, renamed: "reflecting(around:)")
+ func reflection(using point: Point) -> Point {
+ reflecting(around: point)
+ }
+}
diff --git a/third-party/Swift2D/Sources/Rect+CoreGraphics.swift b/third-party/Swift2D/Sources/Rect+CoreGraphics.swift
new file mode 100644
index 0000000000..45eb402e3b
--- /dev/null
+++ b/third-party/Swift2D/Sources/Rect+CoreGraphics.swift
@@ -0,0 +1,32 @@
+#if canImport(CoreGraphics)
+import CoreGraphics
+#else
+import Foundation
+#endif
+
+public extension Rect {
+ /// Initialize a `Rect` using the provided `CGRect` values.
+ init(_ rect: CGRect) {
+ self.init(
+ origin: Point(rect.origin),
+ size: Size(rect.size)
+ )
+ }
+
+ @available(*, deprecated, renamed: "CGRect(_:)", message: "Use CGRect initializer directly")
+ var cgRect: CGRect {
+ CGRect(self)
+ }
+}
+
+public extension CGRect {
+ /// Initialize a `CGRect` using the provided `Rect` values.
+ init(_ rect: Rect) {
+ self.init(
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height
+ )
+ }
+}
diff --git a/third-party/Swift2D/Sources/Rect.swift b/third-party/Swift2D/Sources/Rect.swift
new file mode 100644
index 0000000000..1fd40a2701
--- /dev/null
+++ b/third-party/Swift2D/Sources/Rect.swift
@@ -0,0 +1,251 @@
+/// The location and dimensions of a rectangle.
+public struct Rect: Hashable, Codable, Sendable, CustomStringConvertible {
+
+ public static let zero: Rect = Rect(origin: .zero, size: .zero)
+ public static let nan: Rect = Rect(origin: .nan, size: .nan)
+ public static let infinite: Rect = Rect(origin: .infinite, size: .infinite)
+ public static let null: Rect = Rect(origin: .null, size: .zero)
+
+ /// A point that specifies the coordinates of the rectangle’s origin.
+ public let origin: Point
+ /// A size that specifies the height and width of the rectangle.
+ public let size: Size
+
+ public init(origin: Point = .zero, size: Size = .zero) {
+ self.origin = origin
+ self.size = size
+ }
+
+ public init(x: Double, y: Double, width: Double, height: Double) {
+ origin = Point(x: x, y: y)
+ size = Size(width: width, height: height)
+ }
+
+ public init(x: Float, y: Float, width: Float, height: Float) {
+ origin = Point(x: x, y: y)
+ size = Size(width: width, height: height)
+ }
+
+ public init(x: Int, y: Int, width: Int, height: Int) {
+ origin = Point(x: x, y: y)
+ size = Size(width: width, height: height)
+ }
+
+ public var description: String { "Rect(origin: \(origin), size: \(size))" }
+ public var isZero: Bool { self == .zero }
+ public var isNaN: Bool { self == .nan }
+ public var isInfinite: Bool { self == .infinite }
+ public var isNull: Bool { origin == .infinite || origin == .null }
+ public var isEmpty: Bool { isNull || width == 0.0 || height == 0.0 }
+
+ /// The x-coordinate of the rectangle origin
+ public var x: Double { origin.x }
+
+ /// The y-coordinate of the rectangle origin
+ public var y: Double { origin.y }
+
+ /// The width of the rectangle.
+ public var width: Double { size.width }
+
+ /// The height of the rectangle.
+ public var height: Double { size.height }
+
+ /// The middle of the `Rect` both horizontally and vertically.
+ public var center: Point { Point(x: midX, y: midY) }
+
+ /// The smallest value for the x-coordinate of the rectangle.
+ public var minX: Double {
+ if size.width < 0.0 {
+ return origin.x - abs(size.width)
+ }
+
+ return origin.x
+ }
+
+ /// The x-coordinate that establishes the center of a rectangle.
+ public var midX: Double { minX + size.widthRadius }
+
+ /// The largest value of the x-coordinate for the rectangle.
+ public var maxX: Double {
+ if size.width < 0.0 {
+ return origin.x
+ }
+
+ return origin.x + size.width
+ }
+
+ /// The smallest value for the y-coordinate of the rectangle.
+ public var minY: Double {
+ if size.height < 0.0 {
+ return origin.y - abs(size.height)
+ }
+
+ return origin.y
+ }
+
+ /// The y-coordinate that establishes the center of the rectangle.
+ public var midY: Double { minY + size.heightRadius }
+
+ /// The largest value for the y-coordinate of the rectangle.
+ public var maxY: Double {
+ if size.height < 0.0 {
+ return origin.y
+ }
+
+ return origin.y + size.height
+ }
+
+ /// Returns a rectangle with a positive width and height.
+ public var standardized: Rect {
+ guard !isNull else {
+ return .null
+ }
+
+ return Rect(x: minX, y: minY, width: abs(width), height: abs(height))
+ }
+
+ public func origin(_ value: Point) -> Rect {
+ Rect(origin: value, size: size)
+ }
+
+ public func size(_ value: Size) -> Rect {
+ Rect(origin: origin, size: value)
+ }
+
+ public func contains(_ point: Point) -> Bool {
+ guard !isNull else {
+ return false
+ }
+
+ guard !isEmpty else {
+ return false
+ }
+
+ return (minX ..< maxX).contains(point.x) && (minY ..< maxY).contains(point.y)
+ }
+
+ public func contains(_ rect: Rect) -> Bool {
+ union(rect) == self
+ }
+
+ public func intersects(_ rect: Rect) -> Bool {
+ !intersection(rect).isNull
+ }
+
+ public func intersection(_ rect: Rect) -> Rect {
+ guard !isNull else {
+ return .null
+ }
+
+ guard !rect.isNull else {
+ return .null
+ }
+
+ let r1 = standardized
+ let r1spanH = r1.minX ... r1.maxX
+ let r1spanV = r1.minY ... r1.maxY
+
+ let r2 = rect.standardized
+ let r2spanH = r2.minX ... r2.maxX
+ let r2spanV = r2.minY ... r2.maxY
+
+ guard r1spanH.overlaps(r2spanH), r2spanV.overlaps(r2spanV) else {
+ return .null
+ }
+
+ let overlapH = r1spanH.clamped(to: r2spanH)
+ let width: Double = if overlapH == r1spanH {
+ r1.width
+ } else if overlapH == r2spanH {
+ r2.width
+ } else {
+ overlapH.upperBound - overlapH.lowerBound
+ }
+
+ let overlapV = r1spanV.clamped(to: r2spanV)
+ let height: Double = if overlapV == r1spanV {
+ r1.height
+ } else if overlapV == r2spanV {
+ r2.height
+ } else {
+ overlapV.upperBound - overlapV.lowerBound
+ }
+
+ return Rect(x: overlapH.lowerBound, y: overlapV.lowerBound, width: width, height: height)
+ }
+
+ public func union(_ rect: Rect) -> Rect {
+ guard !isNull else {
+ return rect
+ }
+
+ guard !rect.isNull else {
+ return self
+ }
+
+ let r1 = standardized
+ let r2 = rect.standardized
+
+ let minX = min(r1.minX, r2.minX)
+ let maxX = max(r1.maxX, r2.maxX)
+ let minY = min(r1.minY, r2.minY)
+ let maxY = max(r1.maxY, r2.maxY)
+
+ return Rect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
+ }
+
+ public func offsetBy(dx: Double, dy: Double) -> Rect {
+ guard !isNull else {
+ return self
+ }
+
+ let rect = standardized
+ return Rect(
+ origin: Point(
+ x: rect.origin.x + dx,
+ y: rect.origin.y + dy
+ ),
+ size: rect.size
+ )
+ }
+
+ public func insetBy(dx: Double, dy: Double) -> Rect {
+ guard !isNull else {
+ return self
+ }
+
+ let normalized = standardized
+ let rect = Rect(
+ origin: Point(
+ x: normalized.x + dx,
+ y: normalized.y + dy
+ ),
+ size: Size(
+ width: normalized.width - (dx * 2),
+ height: normalized.height - (dy * 2)
+ )
+ )
+
+ guard rect.size.width >= 0, rect.size.height >= 0 else {
+ return .null
+ }
+
+ return rect
+ }
+
+ public func expandedBy(dx: Double, dy: Double) -> Rect {
+ insetBy(dx: -dx, dy: -dy)
+ }
+}
+
+public extension Rect {
+ @available(*, deprecated, renamed: "origin(_:)")
+ func with(origin value: Point) -> Rect {
+ Rect(origin: value, size: size)
+ }
+
+ @available(*, deprecated, renamed: "size(_:)")
+ func with(size value: Size) -> Rect {
+ Rect(origin: origin, size: value)
+ }
+}
diff --git a/third-party/Swift2D/Sources/Size+CoreGraphics.swift b/third-party/Swift2D/Sources/Size+CoreGraphics.swift
new file mode 100644
index 0000000000..014ba8ace4
--- /dev/null
+++ b/third-party/Swift2D/Sources/Size+CoreGraphics.swift
@@ -0,0 +1,19 @@
+#if canImport(CoreGraphics)
+import CoreGraphics
+#else
+import Foundation
+#endif
+
+public extension Size {
+ /// Initialize a `Size` using the provided `CGSize` values.
+ init(_ size: CGSize) {
+ self.init(width: size.width, height: size.height)
+ }
+}
+
+public extension CGSize {
+ /// Initialize a `CGSize` using the provided `Size` values.
+ init(_ size: Size) {
+ self.init(width: size.width, height: size.height)
+ }
+}
diff --git a/third-party/Swift2D/Sources/Size.swift b/third-party/Swift2D/Sources/Size.swift
new file mode 100644
index 0000000000..202cfa5f36
--- /dev/null
+++ b/third-party/Swift2D/Sources/Size.swift
@@ -0,0 +1,87 @@
+/// A representation of two-dimensional width and height values.
+public struct Size: Hashable, Codable, Sendable, CustomStringConvertible {
+
+ public static let zero: Size = Size(width: 0.0, height: 0.0)
+ public static let nan: Size = Size(width: Double.nan, height: Double.nan)
+ public static let infinite: Size = Size(width: Double.greatestFiniteMagnitude, height: Double.greatestFiniteMagnitude)
+
+ public let width: Double
+ public let height: Double
+
+ public init(width: Double = 0.0, height: Double = 0.0) {
+ self.width = width
+ self.height = height
+ }
+
+ public init(width: Float, height: Float) {
+ self.width = Double(width)
+ self.height = Double(height)
+ }
+
+ public init(width: Int, height: Int) {
+ self.width = Double(width)
+ self.height = Double(height)
+ }
+
+ public var description: String { "Size(width: \(width), height: \(height))" }
+ public var isZero: Bool { self == .zero }
+ public var isNaN: Bool { self == .nan }
+ public var isInfinite: Bool { self == .infinite }
+
+ /// The radius defined by the `width` dimension.
+ public var widthRadius: Double { abs(width) / 2.0 }
+
+ /// The radius defined by the `height` dimension.
+ public var heightRadius: Double { abs(height) / 2.0 }
+
+ /// The largest of the width and height radii.
+ public var maxRadius: Double { max(widthRadius, heightRadius) }
+
+ /// The smallest of the width and height radii.
+ public var minRadius: Double { min(widthRadius, heightRadius) }
+
+ /// The `Point` at which the `widthRadius` & `heightRadius` intersect.
+ public var center: Point { Point(x: widthRadius, y: heightRadius) }
+
+ /// Create a new instance maintaining the `height` value while using the provided `width` value.
+ public func width(_ value: Double) -> Size {
+ Size(width: value, height: height)
+ }
+
+ /// Create a new instance maintaining the `width` value while using the provided `height` value.
+ public func height(_ value: Double) -> Size {
+ Size(width: width, height: value)
+ }
+
+ public static func == (lhs: Size, rhs: Size) -> Bool {
+ if lhs.width.isNaN, rhs.width.isNaN, lhs.height.isNaN, rhs.height.isNaN {
+ return true
+ }
+
+ return lhs.width == rhs.width && lhs.height == rhs.height
+ }
+}
+
+public extension Size {
+ @available(*, deprecated, renamed: "widthRadius")
+ var xRadius: Double { abs(width) / 2.0 }
+
+ @available(*, deprecated, renamed: "heightRadius")
+ var yRadius: Double { abs(height) / 2.0 }
+
+ @available(*, deprecated, renamed: "widthRadius")
+ var horizontalRadius: Double { xRadius }
+
+ @available(*, deprecated, renamed: "heightRadius")
+ var verticalRadius: Double { yRadius }
+
+ @available(*, deprecated, renamed: "width(_:)")
+ func with(width value: Double) -> Size {
+ width(value)
+ }
+
+ @available(*, deprecated, renamed: "height(_:)")
+ func with(height value: Double) -> Size {
+ height(value)
+ }
+}
diff --git a/third-party/SwiftColor/BUILD b/third-party/SwiftColor/BUILD
new file mode 100644
index 0000000000..a45048f861
--- /dev/null
+++ b/third-party/SwiftColor/BUILD
@@ -0,0 +1,17 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "SwiftColor",
+ module_name = "SwiftColor",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-suppress-warnings",
+ ],
+ deps = [
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/third-party/SwiftColor/Sources/Clamping.swift b/third-party/SwiftColor/Sources/Clamping.swift
new file mode 100644
index 0000000000..4154946f98
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Clamping.swift
@@ -0,0 +1,20 @@
+@propertyWrapper
+public struct Clamping {
+ var value: Value
+ let range: ClosedRange
+
+ public init(wrappedValue value: Value, _ range: ClosedRange) {
+ precondition(range.contains(value))
+ self.value = value
+ self.range = range
+ }
+
+ public var wrappedValue: Value {
+ get {
+ value
+ }
+ set(newValue) {
+ value = min(max(range.lowerBound, newValue), range.upperBound)
+ }
+ }
+}
diff --git a/third-party/SwiftColor/Sources/ColorSpace.swift b/third-party/SwiftColor/Sources/ColorSpace.swift
new file mode 100644
index 0000000000..fef838c450
--- /dev/null
+++ b/third-party/SwiftColor/Sources/ColorSpace.swift
@@ -0,0 +1,3 @@
+public enum ColorSpace: Equatable, Sendable {
+ case rgba
+}
diff --git a/third-party/SwiftColor/Sources/Pigment+AppKit.swift b/third-party/SwiftColor/Sources/Pigment+AppKit.swift
new file mode 100644
index 0000000000..38c875d965
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment+AppKit.swift
@@ -0,0 +1,24 @@
+import Foundation
+#if canImport(AppKit) && !targetEnvironment(macCatalyst)
+import AppKit
+
+public extension Pigment {
+ /// Initialize a `Pigment` using an `NSColor`.
+ init(_ color: NSColor) {
+ red = color.redComponent
+ green = color.greenComponent
+ blue = color.blueComponent
+ alpha = color.alphaComponent
+ }
+
+ var nsColor: NSColor {
+ NSColor(red: red, green: green, blue: blue, alpha: alpha)
+ }
+}
+
+public extension NSColor {
+ var pigment: Pigment {
+ Pigment(self)
+ }
+}
+#endif
diff --git a/third-party/SwiftColor/Sources/Pigment+CoreGraphics.swift b/third-party/SwiftColor/Sources/Pigment+CoreGraphics.swift
new file mode 100644
index 0000000000..29eaba236e
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment+CoreGraphics.swift
@@ -0,0 +1,39 @@
+import Foundation
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+public extension Pigment {
+ /// Initialize a `Pigment` using an `CGColor`.
+ init?(_ color: CGColor) {
+ let components = color.components ?? []
+ switch components.count {
+ case 2:
+ // Monochrome/Grayscale
+ // TODO: Express this in 'Color'.
+ red = 0.0
+ green = 0.0
+ blue = 0.0
+ alpha = components[1]
+ case 4:
+ // RGB
+ red = components[0]
+ green = components[1]
+ blue = components[2]
+ alpha = components[3]
+ default:
+ return nil
+ }
+ }
+}
+
+public extension CGColor {
+ static func make(_ color: Pigment) -> CGColor {
+ if color.red == 0.0, color.green == 0.0, color.blue == 0.0, color.alpha == 0.0 {
+ // Return the CG color equivalent of `UIColor.clear`.
+ return CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.0)
+ }
+
+ return CGColor(srgbRed: color.red, green: color.green, blue: color.blue, alpha: color.alpha)
+ }
+}
+#endif
diff --git a/third-party/SwiftColor/Sources/Pigment+Float.swift b/third-party/SwiftColor/Sources/Pigment+Float.swift
new file mode 100644
index 0000000000..fbcaf7001a
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment+Float.swift
@@ -0,0 +1,95 @@
+import Foundation
+
+public extension Pigment {
+ /// Initialize a `Pigment` using `Float` values leaning towards the 'Red' spectrum.
+ ///
+ /// - parameters
+ /// - red: A value in the range of 0.0 to 1.0 representing the **red** percent.
+ /// - green: A value in the range of 0.0 to 1.0 representing the **green** percent.
+ /// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent.
+ /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
+ init(
+ red: Float,
+ green: Float = 0.0,
+ blue: Float = 0.0,
+ alpha: Float = 1.0
+ ) {
+ self.red = Double(red)
+ self.green = Double(green)
+ self.blue = Double(blue)
+ self.alpha = Double(alpha)
+ }
+
+ /// Initialize a `Pigment` using `Float` values leaning towards the 'Green' spectrum.
+ ///
+ /// - parameters:
+ /// - green: A value in the range of 0.0 to 1.0 representing the **green** percent.
+ /// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent.
+ /// - red: A value in the range of 0.0 to 1.0 representing the **red** percent.
+ /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
+ init(
+ green: Float,
+ blue: Float = 0.0,
+ red: Float = 0.0,
+ alpha: Float = 1.0
+ ) {
+ self.red = Double(red)
+ self.green = Double(green)
+ self.blue = Double(blue)
+ self.alpha = Double(alpha)
+ }
+
+ /// Initialize a `Pigment` using `Float` values leaning towards the 'Blue' spectrum.
+ ///
+ /// - parameter:
+ /// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent.
+ /// - red: A value in the range of 0.0 to 1.0 representing the **red** percent.
+ /// - green: A value in the range of 0.0 to 1.0 representing the **green** percent.
+ /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
+ init(
+ blue: Float,
+ red: Float = 0.0,
+ green: Float = 0.0,
+ alpha: Float = 1.0
+ ) {
+ self.red = Double(red)
+ self.green = Double(green)
+ self.blue = Double(blue)
+ self.alpha = Double(alpha)
+ }
+
+ /// Initialize a `Pigment` using variadic `Float` values.
+ ///
+ /// All _values_ should be expressed in the range of 0.0 to 1.0.
+ ///
+ /// - parameters:
+ /// - values: A number of `Float` which are mapped to **red**, **green**, **blue** in that order.
+ /// - alpha: Amount of _opacity/transparency_ to apply.
+ init(
+ _ values: Float...,
+ alpha: Float
+ ) {
+ if values.count > 0 {
+ red = Double(values[0].clamped(to: 0 ... 1))
+ } else {
+ red = 0.0
+ }
+ if values.count > 1 {
+ green = Double(values[1].clamped(to: 0 ... 1))
+ } else {
+ green = 0.0
+ }
+ if values.count > 2 {
+ blue = Double(values[2].clamped(to: 0 ... 1))
+ } else {
+ blue = 0.0
+ }
+ self.alpha = Double(alpha.clamped(to: 0 ... 1))
+ }
+}
+
+extension Float {
+ func clamped(to range: ClosedRange) -> Float {
+ min(max(range.lowerBound, self), range.upperBound)
+ }
+}
diff --git a/third-party/SwiftColor/Sources/Pigment+Hex.swift b/third-party/SwiftColor/Sources/Pigment+Hex.swift
new file mode 100644
index 0000000000..c969b9538c
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment+Hex.swift
@@ -0,0 +1,145 @@
+import Foundation
+
+public extension Pigment {
+ /// Initializes a `Pigment` with an `Int` in the expected format of **0x000**.
+ ///
+ /// Used as a short-hand. If the hex 0x123 is provided, it is interpreted as 0x112233.
+ init(
+ hex3 hex: Int,
+ @Clamping(0 ... 1) alpha: Double = 1.0
+ ) {
+ let values = Self.hex3(hex: hex, alpha: alpha)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ }
+
+ /// Shorthand **0x0000** initializer similar to `init(hex3:alpha:)` where
+ /// the last digit represent the alpha component.
+ init(
+ hex4 hex: Int
+ ) {
+ let values = Self.hex4(hex: hex)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ alpha = values.alpha
+ }
+
+ /// Initializes with a standard format hex representation of color in the form of **0x1E2C3D**.
+ init(
+ hex6 hex: Int,
+ @Clamping(0 ... 1) alpha: Double = 1.0
+ ) {
+ let values = Self.hex6(hex: hex, alpha: alpha)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ }
+
+ #if !os(watchOS)
+ /// Extended form of `init(hex6:alpha:)` expecting **0x112233FF**, that uses the last
+ /// bits for the alpha component.
+ init(
+ hex8 hex: Int
+ ) {
+ let values = Self.hex8(hex: hex)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ alpha = values.alpha
+ }
+ #endif
+
+ /// Initializes a `Pigment` with an `Int` representation of an RGB(a) Hex Value
+ ///
+ /// This initializer will do its best to interpret the intentions of what is provided.
+ /// **YOUR RESULTS WILL VARY**, and it's best to use one of the `init(hex?:)` initializers.
+ ///
+ /// - Parameter hex: Hex value
+ /// - Parameter alpha: The opacity value of the color object
+ init(
+ _ hex: Int,
+ alpha: Double? = nil
+ ) {
+ #if !os(watchOS)
+ if hex > 0xFFFFFF {
+ let values = Self.hex8(hex: hex)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ return
+ }
+ #endif
+ if hex > 0xFFFF {
+ let values = Self.hex6(hex: hex, alpha: alpha ?? 1.0)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ } else if hex > 0xFFF {
+ let values = Self.hex4(hex: hex)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ } else {
+ let values = Self.hex3(hex: hex, alpha: alpha ?? 1.0)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ }
+ }
+}
+
+extension Pigment {
+ static func hex3(
+ hex: Int,
+ @Clamping(0 ... 1) alpha: Double = 1.0
+ ) -> (red: Double, green: Double, blue: Double, alpha: Double) {
+ let red = Double(duplicateBits((hex & 0xF00) >> 8)) / 255.0
+ let green = Double(duplicateBits((hex & 0x0F0) >> 4)) / 255.0
+ let blue = Double(duplicateBits((hex & 0x00F) >> 0)) / 255.0
+ return (red, green, blue, alpha)
+ }
+
+ static func hex4(
+ hex: Int
+ ) -> (red: Double, green: Double, blue: Double, alpha: Double) {
+ let red = Double(duplicateBits((hex & 0xF000) >> 12)) / 255.0
+ let green = Double(duplicateBits((hex & 0x0F00) >> 8)) / 255.0
+ let blue = Double(duplicateBits((hex & 0x00F0) >> 4)) / 255.0
+ let alpha = Double(duplicateBits((hex & 0x000F) >> 0)) / 255.0
+ return (red, green, blue, alpha)
+ }
+
+ static func hex6(
+ hex: Int,
+ @Clamping(0 ... 1) alpha: Double = 1.0
+ ) -> (red: Double, green: Double, blue: Double, alpha: Double) {
+ let red = Double((hex & 0xFF0000) >> 16) / 255.0
+ let green = Double((hex & 0x00FF00) >> 8) / 255.0
+ let blue = Double((hex & 0x0000FF) >> 0) / 255.0
+ return (red, green, blue, alpha)
+ }
+
+ #if !os(watchOS)
+ static func hex8(
+ hex: Int
+ ) -> (red: Double, green: Double, blue: Double, alpha: Double) {
+ let red = Double((hex & 0xFF00_0000) >> 24) / 255.0
+ let green = Double((hex & 0x00FF_0000) >> 16) / 255.0
+ let blue = Double((hex & 0x0000_FF00) >> 8) / 255.0
+ let alpha = Double((hex & 0x0000_00FF) >> 0) / 255.0
+ return (red, green, blue, alpha)
+ }
+ #endif
+
+ static func duplicateBits(_ value: Int) -> Int {
+ (value << 4) + value
+ }
+}
diff --git a/third-party/SwiftColor/Sources/Pigment+Int.swift b/third-party/SwiftColor/Sources/Pigment+Int.swift
new file mode 100644
index 0000000000..16568d0142
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment+Int.swift
@@ -0,0 +1,89 @@
+import Foundation
+
+public extension Pigment {
+ /// Initialize a `Pigment` using `Int` values leaning towards the 'Red' spectrum.
+ ///
+ /// - parameters:
+ /// - red: A value in the range of 0 to 255 representing the **red** bits
+ /// - green: A value in the range of 0 to 255 representing the **green** bits
+ /// - blue: A value in the range of 0 to 255 representing the **blue** bits
+ /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
+ init(
+ @Clamping(0 ... 255) red: Int,
+ @Clamping(0 ... 255) green: Int = 0,
+ @Clamping(0 ... 255) blue: Int = 0,
+ @Clamping(0.0 ... 1.0) alpha: Double = 1.0
+ ) {
+ self.red = Double(red) / 255.0
+ self.green = Double(green) / 255.0
+ self.blue = Double(blue) / 255.0
+ self.alpha = alpha
+ }
+
+ /// Initialize a `Pigment` using `Int` values leaning towards the 'Green' spectrum.
+ ///
+ /// - parameters:
+ /// - green: A value in the range of 0 to 255 representing the **green** bits
+ /// - blue: A value in the range of 0 to 255 representing the **blue** bits
+ /// - red: A value in the range of 0 to 255 representing the **red** bits
+ /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
+ init(
+ @Clamping(0 ... 255) green: Int,
+ @Clamping(0 ... 255) blue: Int = 0,
+ @Clamping(0 ... 255) red: Int = 0,
+ @Clamping(0.0 ... 1.0) alpha: Double = 1.0
+ ) {
+ self.red = Double(red) / 255.0
+ self.green = Double(green) / 255.0
+ self.blue = Double(blue) / 255.0
+ self.alpha = alpha
+ }
+
+ /// Initialize a `Pigment` using `Int` values leaning towards the 'Blue' spectrum.
+ ///
+ /// - parameters:
+ /// - blue: A value in the range of 0 to 255 representing the **blue** bits
+ /// - red: A value in the range of 0 to 255 representing the **red** bits
+ /// - green: A value in the range of 0 to 255 representing the **green** bits
+ /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
+ init(
+ @Clamping(0 ... 255) blue: Int,
+ @Clamping(0 ... 255) red: Int = 0,
+ @Clamping(0 ... 255) green: Int = 0,
+ @Clamping(0.0 ... 1.0) alpha: Double = 1.0
+ ) {
+ self.red = Double(red) / 255.0
+ self.green = Double(green) / 255.0
+ self.blue = Double(blue) / 255.0
+ self.alpha = alpha
+ }
+
+ /// Initialize a `Pigment` using variadic `Int` values.
+ ///
+ /// All _values_ should be expressed in the range of 0 to 255.
+ ///
+ /// - parameters:
+ /// - values: A number of `Int` which are mapped to **red**, **green**, **blue** in that order.
+ /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
+ init(
+ _ values: Int...,
+ @Clamping(0 ... 1) alpha: Double
+ ) {
+ if values.count > 0 {
+ red = Double(values[0]) / 255.0
+ } else {
+ red = 0.0
+ }
+ if values.count > 1 {
+ green = Double(values[1]) / 255.0
+ } else {
+ green = 0.0
+ }
+ if values.count > 2 {
+ blue = Double(values[2]) / 255.0
+ } else {
+ blue = 0.0
+ }
+ self.alpha = alpha
+ }
+}
diff --git a/third-party/SwiftColor/Sources/Pigment+Name.swift b/third-party/SwiftColor/Sources/Pigment+Name.swift
new file mode 100644
index 0000000000..8857f8f450
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment+Name.swift
@@ -0,0 +1,470 @@
+import Foundation
+
+public extension Pigment {
+ @available(*, deprecated, renamed: "Name")
+ typealias Keyword = Name
+
+ enum Name: String, CaseIterable {
+ case aliceBlue
+ case antiqueWhite
+ case aqua
+ case aquamarine
+ case azure
+ case beige
+ case bisque
+ case black
+ case blanchedAlmond
+ case blue
+ case blueViolet
+ case brown
+ case burlywood
+ case cadetBlue
+ case chartreuse
+ case chocolate
+ case coral
+ case cornflowerBlue
+ case cornsilk
+ case crimson
+ case cyan
+ case darkBlue
+ case darkCyan
+ case darkGoldenrod
+ case darkGray
+ case darkGreen
+ case darkGrey
+ case darkKhaki
+ case darkMagenta
+ case darkOliveGreen
+ case darkOrange
+ case darkOrchid
+ case darkRed
+ case darkSalmon
+ case darkSeagreen
+ case darkSlateBlue
+ case darkSlateGray
+ case darkSlateGrey
+ case darkTurquoise
+ case darkViolet
+ case deepPink
+ case deepSkyblue
+ case dimGray
+ case dimGrey
+ case dodgerBlue
+ case firebrick
+ case floralWhite
+ case forestGreen
+ case fuchsia
+ case gainsboro
+ case ghostWhite
+ case gold
+ case goldenrod
+ case gray
+ case green
+ case greenYellow
+ case grey
+ case honeydew
+ case hotPink
+ case indianRed
+ case indigo
+ case ivory
+ case khaki
+ case lavender
+ case lavenderBlush
+ case lawnGreen
+ case lemonChiffon
+ case lightBlue
+ case lightCoral
+ case lightCyan
+ case lightGoldenrodYellow
+ case lightGray
+ case lightGreen
+ case lightGrey
+ case lightPink
+ case lightSalmon
+ case lightSeagreen
+ case lightSkyBlue
+ case lightSlateGray
+ case lightSlateGrey
+ case lightSteelBlue
+ case lightYellow
+ case lime
+ case limeGreen
+ case linen
+ case magenta
+ case maroon
+ case mediumAquamarine
+ case mediumBlue
+ case mediumOrchid
+ case mediumPurple
+ case mediumSeagreen
+ case mediumSlateBlue
+ case mediumSpringGreen
+ case mediumTurquoise
+ case mediumVioletRed
+ case midnightBlue
+ case mintCream
+ case mistyRose
+ case moccasin
+ case navajoWhite
+ case navy
+ case oldLace
+ case olive
+ case oliveDrab
+ case orange
+ case orangeRed
+ case orchid
+ case paleGoldenrod
+ case paleGreen
+ case paleTurquoise
+ case paleVioletRed
+ case papayaWhip
+ case peachPuff
+ case peru
+ case pink
+ case plum
+ case powderBlue
+ case purple
+ case red
+ case rosyBrown
+ case royalBlue
+ case saddleBrown
+ case salmon
+ case sandyBrown
+ case seagreen
+ case seashell
+ case sienna
+ case silver
+ case skyBlue
+ case slateBlue
+ case slateGray
+ case slateGrey
+ case snow
+ case springGreen
+ case steelBlue
+ case tan
+ case teal
+ case thistle
+ case tomato
+ case turquoise
+ case violet
+ case wheat
+ case white
+ case whitesmoke
+ case yellow
+ case yellowGreen
+
+ public var rgb: (red: Int, green: Int, blue: Int) {
+ switch self {
+ case .aliceBlue: (240, 248, 255)
+ case .antiqueWhite: (250, 235, 215)
+ case .aqua: (0, 255, 255)
+ case .aquamarine: (127, 255, 212)
+ case .azure: (240, 255, 255)
+ case .beige: (245, 245, 220)
+ case .bisque: (255, 228, 196)
+ case .black: (0, 0, 0)
+ case .blanchedAlmond: (255, 235, 205)
+ case .blue: (0, 0, 255)
+ case .blueViolet: (138, 43, 226)
+ case .brown: (165, 42, 42)
+ case .burlywood: (222, 184, 135)
+ case .cadetBlue: (95, 158, 160)
+ case .chartreuse: (127, 255, 0)
+ case .chocolate: (210, 105, 30)
+ case .coral: (255, 127, 80)
+ case .cornflowerBlue: (100, 149, 237)
+ case .cornsilk: (255, 248, 220)
+ case .crimson: (220, 20, 60)
+ case .cyan: (0, 255, 255)
+ case .darkBlue: (0, 0, 139)
+ case .darkCyan: (0, 139, 139)
+ case .darkGoldenrod: (184, 134, 11)
+ case .darkGray: (169, 169, 169)
+ case .darkGreen: (0, 100, 0)
+ case .darkGrey: (169, 169, 169)
+ case .darkKhaki: (189, 183, 107)
+ case .darkMagenta: (139, 0, 139)
+ case .darkOliveGreen: (85, 107, 47)
+ case .darkOrange: (255, 140, 0)
+ case .darkOrchid: (153, 50, 204)
+ case .darkRed: (139, 0, 0)
+ case .darkSalmon: (233, 150, 122)
+ case .darkSeagreen: (143, 188, 143)
+ case .darkSlateBlue: (72, 61, 139)
+ case .darkSlateGray: (47, 79, 79)
+ case .darkSlateGrey: (47, 79, 79)
+ case .darkTurquoise: (0, 206, 209)
+ case .darkViolet: (148, 0, 211)
+ case .deepPink: (255, 20, 147)
+ case .deepSkyblue: (0, 191, 255)
+ case .dimGray: (105, 105, 105)
+ case .dimGrey: (105, 105, 105)
+ case .dodgerBlue: (30, 144, 255)
+ case .firebrick: (178, 34, 34)
+ case .floralWhite: (255, 250, 240)
+ case .forestGreen: (34, 139, 34)
+ case .fuchsia: (255, 0, 255)
+ case .gainsboro: (220, 220, 220)
+ case .ghostWhite: (248, 248, 255)
+ case .gold: (255, 215, 0)
+ case .goldenrod: (218, 165, 32)
+ case .gray: (128, 128, 128)
+ case .green: (0, 128, 0)
+ case .greenYellow: (173, 255, 47)
+ case .grey: (128, 128, 128)
+ case .honeydew: (240, 255, 240)
+ case .hotPink: (255, 105, 180)
+ case .indianRed: (205, 92, 92)
+ case .indigo: (75, 0, 130)
+ case .ivory: (255, 255, 240)
+ case .khaki: (240, 230, 140)
+ case .lavender: (230, 230, 250)
+ case .lavenderBlush: (255, 240, 245)
+ case .lawnGreen: (124, 252, 0)
+ case .lemonChiffon: (255, 250, 205)
+ case .lightBlue: (173, 216, 230)
+ case .lightCoral: (240, 128, 128)
+ case .lightCyan: (224, 255, 255)
+ case .lightGoldenrodYellow: (250, 250, 210)
+ case .lightGray: (211, 211, 211)
+ case .lightGreen: (144, 238, 144)
+ case .lightGrey: (211, 211, 211)
+ case .lightPink: (255, 182, 193)
+ case .lightSalmon: (255, 160, 122)
+ case .lightSeagreen: (32, 178, 170)
+ case .lightSkyBlue: (135, 206, 250)
+ case .lightSlateGray: (119, 136, 153)
+ case .lightSlateGrey: (119, 136, 153)
+ case .lightSteelBlue: (176, 196, 222)
+ case .lightYellow: (255, 255, 224)
+ case .lime: (0, 255, 0)
+ case .limeGreen: (50, 205, 50)
+ case .linen: (250, 240, 230)
+ case .magenta: (255, 0, 255)
+ case .maroon: (128, 0, 0)
+ case .mediumAquamarine: (102, 205, 170)
+ case .mediumBlue: (0, 0, 205)
+ case .mediumOrchid: (186, 85, 211)
+ case .mediumPurple: (147, 112, 219)
+ case .mediumSeagreen: (60, 179, 113)
+ case .mediumSlateBlue: (123, 104, 238)
+ case .mediumSpringGreen: (0, 250, 154)
+ case .mediumTurquoise: (72, 209, 204)
+ case .mediumVioletRed: (199, 21, 133)
+ case .midnightBlue: (25, 25, 112)
+ case .mintCream: (245, 255, 250)
+ case .mistyRose: (255, 228, 225)
+ case .moccasin: (255, 228, 181)
+ case .navajoWhite: (255, 222, 173)
+ case .navy: (0, 0, 128)
+ case .oldLace: (253, 245, 230)
+ case .olive: (128, 128, 0)
+ case .oliveDrab: (107, 142, 35)
+ case .orange: (255, 165, 0)
+ case .orangeRed: (255, 69, 0)
+ case .orchid: (218, 112, 214)
+ case .paleGoldenrod: (238, 232, 170)
+ case .paleGreen: (152, 251, 152)
+ case .paleTurquoise: (175, 238, 238)
+ case .paleVioletRed: (219, 112, 147)
+ case .papayaWhip: (255, 239, 213)
+ case .peachPuff: (255, 218, 185)
+ case .peru: (205, 133, 63)
+ case .pink: (255, 192, 203)
+ case .plum: (221, 160, 221)
+ case .powderBlue: (176, 224, 230)
+ case .purple: (128, 0, 128)
+ case .red: (255, 0, 0)
+ case .rosyBrown: (188, 143, 143)
+ case .royalBlue: (65, 105, 225)
+ case .saddleBrown: (139, 69, 19)
+ case .salmon: (250, 128, 114)
+ case .sandyBrown: (244, 164, 96)
+ case .seagreen: (46, 139, 87)
+ case .seashell: (255, 245, 238)
+ case .sienna: (160, 82, 45)
+ case .silver: (192, 192, 192)
+ case .skyBlue: (135, 206, 235)
+ case .slateBlue: (106, 90, 205)
+ case .slateGray: (112, 128, 144)
+ case .slateGrey: (112, 128, 144)
+ case .snow: (255, 250, 250)
+ case .springGreen: (0, 255, 127)
+ case .steelBlue: (70, 130, 180)
+ case .tan: (210, 180, 140)
+ case .teal: (0, 128, 128)
+ case .thistle: (216, 191, 216)
+ case .tomato: (255, 99, 71)
+ case .turquoise: (64, 224, 208)
+ case .violet: (238, 130, 238)
+ case .wheat: (245, 222, 179)
+ case .white: (255, 255, 255)
+ case .whitesmoke: (245, 245, 245)
+ case .yellow: (255, 255, 0)
+ case .yellowGreen: (154, 205, 50)
+ }
+ }
+
+ public var pigment: Pigment {
+ switch self {
+ case .aliceBlue: Pigment(240, 248, 255, alpha: 1.0)
+ case .antiqueWhite: Pigment(250, 235, 215, alpha: 1.0)
+ case .aqua: Pigment(0, 255, 255, alpha: 1.0)
+ case .aquamarine: Pigment(127, 255, 212, alpha: 1.0)
+ case .azure: Pigment(240, 255, 255, alpha: 1.0)
+ case .beige: Pigment(245, 245, 220, alpha: 1.0)
+ case .bisque: Pigment(255, 228, 196, alpha: 1.0)
+ case .black: Pigment(0, 0, 0, alpha: 1.0)
+ case .blanchedAlmond: Pigment(255, 235, 205, alpha: 1.0)
+ case .blue: Pigment(0, 0, 255, alpha: 1.0)
+ case .blueViolet: Pigment(138, 43, 226, alpha: 1.0)
+ case .brown: Pigment(165, 42, 42, alpha: 1.0)
+ case .burlywood: Pigment(222, 184, 135, alpha: 1.0)
+ case .cadetBlue: Pigment(95, 158, 160, alpha: 1.0)
+ case .chartreuse: Pigment(127, 255, 0, alpha: 1.0)
+ case .chocolate: Pigment(210, 105, 30, alpha: 1.0)
+ case .coral: Pigment(255, 127, 80, alpha: 1.0)
+ case .cornflowerBlue: Pigment(100, 149, 237, alpha: 1.0)
+ case .cornsilk: Pigment(255, 248, 220, alpha: 1.0)
+ case .crimson: Pigment(220, 20, 60, alpha: 1.0)
+ case .cyan: Pigment(0, 255, 255, alpha: 1.0)
+ case .darkBlue: Pigment(0, 0, 139, alpha: 1.0)
+ case .darkCyan: Pigment(0, 139, 139, alpha: 1.0)
+ case .darkGoldenrod: Pigment(184, 134, 11, alpha: 1.0)
+ case .darkGray: Pigment(169, 169, 169, alpha: 1.0)
+ case .darkGreen: Pigment(0, 100, 0, alpha: 1.0)
+ case .darkGrey: Pigment(169, 169, 169, alpha: 1.0)
+ case .darkKhaki: Pigment(189, 183, 107, alpha: 1.0)
+ case .darkMagenta: Pigment(139, 0, 139, alpha: 1.0)
+ case .darkOliveGreen: Pigment(85, 107, 47, alpha: 1.0)
+ case .darkOrange: Pigment(255, 140, 0, alpha: 1.0)
+ case .darkOrchid: Pigment(153, 50, 204, alpha: 1.0)
+ case .darkRed: Pigment(139, 0, 0, alpha: 1.0)
+ case .darkSalmon: Pigment(233, 150, 122, alpha: 1.0)
+ case .darkSeagreen: Pigment(143, 188, 143, alpha: 1.0)
+ case .darkSlateBlue: Pigment(72, 61, 139, alpha: 1.0)
+ case .darkSlateGray: Pigment(47, 79, 79, alpha: 1.0)
+ case .darkSlateGrey: Pigment(47, 79, 79, alpha: 1.0)
+ case .darkTurquoise: Pigment(0, 206, 209, alpha: 1.0)
+ case .darkViolet: Pigment(148, 0, 211, alpha: 1.0)
+ case .deepPink: Pigment(255, 20, 147, alpha: 1.0)
+ case .deepSkyblue: Pigment(0, 191, 255, alpha: 1.0)
+ case .dimGray: Pigment(105, 105, 105, alpha: 1.0)
+ case .dimGrey: Pigment(105, 105, 105, alpha: 1.0)
+ case .dodgerBlue: Pigment(30, 144, 255, alpha: 1.0)
+ case .firebrick: Pigment(178, 34, 34, alpha: 1.0)
+ case .floralWhite: Pigment(255, 250, 240, alpha: 1.0)
+ case .forestGreen: Pigment(34, 139, 34, alpha: 1.0)
+ case .fuchsia: Pigment(255, 0, 255, alpha: 1.0)
+ case .gainsboro: Pigment(220, 220, 220, alpha: 1.0)
+ case .ghostWhite: Pigment(248, 248, 255, alpha: 1.0)
+ case .gold: Pigment(255, 215, 0, alpha: 1.0)
+ case .goldenrod: Pigment(218, 165, 32, alpha: 1.0)
+ case .gray: Pigment(128, 128, 128, alpha: 1.0)
+ case .green: Pigment(0, 128, 0, alpha: 1.0)
+ case .greenYellow: Pigment(173, 255, 47, alpha: 1.0)
+ case .grey: Pigment(128, 128, 128, alpha: 1.0)
+ case .honeydew: Pigment(240, 255, 240, alpha: 1.0)
+ case .hotPink: Pigment(255, 105, 180, alpha: 1.0)
+ case .indianRed: Pigment(205, 92, 92, alpha: 1.0)
+ case .indigo: Pigment(75, 0, 130, alpha: 1.0)
+ case .ivory: Pigment(255, 255, 240, alpha: 1.0)
+ case .khaki: Pigment(240, 230, 140, alpha: 1.0)
+ case .lavender: Pigment(230, 230, 250, alpha: 1.0)
+ case .lavenderBlush: Pigment(255, 240, 245, alpha: 1.0)
+ case .lawnGreen: Pigment(124, 252, 0, alpha: 1.0)
+ case .lemonChiffon: Pigment(255, 250, 205, alpha: 1.0)
+ case .lightBlue: Pigment(173, 216, 230, alpha: 1.0)
+ case .lightCoral: Pigment(240, 128, 128, alpha: 1.0)
+ case .lightCyan: Pigment(224, 255, 255, alpha: 1.0)
+ case .lightGoldenrodYellow: Pigment(250, 250, 210, alpha: 1.0)
+ case .lightGray: Pigment(211, 211, 211, alpha: 1.0)
+ case .lightGreen: Pigment(144, 238, 144, alpha: 1.0)
+ case .lightGrey: Pigment(211, 211, 211, alpha: 1.0)
+ case .lightPink: Pigment(255, 182, 193, alpha: 1.0)
+ case .lightSalmon: Pigment(255, 160, 122, alpha: 1.0)
+ case .lightSeagreen: Pigment(32, 178, 170, alpha: 1.0)
+ case .lightSkyBlue: Pigment(135, 206, 250, alpha: 1.0)
+ case .lightSlateGray: Pigment(119, 136, 153, alpha: 1.0)
+ case .lightSlateGrey: Pigment(119, 136, 153, alpha: 1.0)
+ case .lightSteelBlue: Pigment(176, 196, 222, alpha: 1.0)
+ case .lightYellow: Pigment(255, 255, 224, alpha: 1.0)
+ case .lime: Pigment(0, 255, 0, alpha: 1.0)
+ case .limeGreen: Pigment(50, 205, 50, alpha: 1.0)
+ case .linen: Pigment(250, 240, 230, alpha: 1.0)
+ case .magenta: Pigment(255, 0, 255, alpha: 1.0)
+ case .maroon: Pigment(128, 0, 0, alpha: 1.0)
+ case .mediumAquamarine: Pigment(102, 205, 170, alpha: 1.0)
+ case .mediumBlue: Pigment(0, 0, 205, alpha: 1.0)
+ case .mediumOrchid: Pigment(186, 85, 211, alpha: 1.0)
+ case .mediumPurple: Pigment(147, 112, 219, alpha: 1.0)
+ case .mediumSeagreen: Pigment(60, 179, 113, alpha: 1.0)
+ case .mediumSlateBlue: Pigment(123, 104, 238, alpha: 1.0)
+ case .mediumSpringGreen: Pigment(0, 250, 154, alpha: 1.0)
+ case .mediumTurquoise: Pigment(72, 209, 204, alpha: 1.0)
+ case .mediumVioletRed: Pigment(199, 21, 133, alpha: 1.0)
+ case .midnightBlue: Pigment(25, 25, 112, alpha: 1.0)
+ case .mintCream: Pigment(245, 255, 250, alpha: 1.0)
+ case .mistyRose: Pigment(255, 228, 225, alpha: 1.0)
+ case .moccasin: Pigment(255, 228, 181, alpha: 1.0)
+ case .navajoWhite: Pigment(255, 222, 173, alpha: 1.0)
+ case .navy: Pigment(0, 0, 128, alpha: 1.0)
+ case .oldLace: Pigment(253, 245, 230, alpha: 1.0)
+ case .olive: Pigment(128, 128, 0, alpha: 1.0)
+ case .oliveDrab: Pigment(107, 142, 35, alpha: 1.0)
+ case .orange: Pigment(255, 165, 0, alpha: 1.0)
+ case .orangeRed: Pigment(255, 69, 0, alpha: 1.0)
+ case .orchid: Pigment(218, 112, 214, alpha: 1.0)
+ case .paleGoldenrod: Pigment(238, 232, 170, alpha: 1.0)
+ case .paleGreen: Pigment(152, 251, 152, alpha: 1.0)
+ case .paleTurquoise: Pigment(175, 238, 238, alpha: 1.0)
+ case .paleVioletRed: Pigment(219, 112, 147, alpha: 1.0)
+ case .papayaWhip: Pigment(255, 239, 213, alpha: 1.0)
+ case .peachPuff: Pigment(255, 218, 185, alpha: 1.0)
+ case .peru: Pigment(205, 133, 63, alpha: 1.0)
+ case .pink: Pigment(255, 192, 203, alpha: 1.0)
+ case .plum: Pigment(221, 160, 221, alpha: 1.0)
+ case .powderBlue: Pigment(176, 224, 230, alpha: 1.0)
+ case .purple: Pigment(128, 0, 128, alpha: 1.0)
+ case .red: Pigment(255, 0, 0, alpha: 1.0)
+ case .rosyBrown: Pigment(188, 143, 143, alpha: 1.0)
+ case .royalBlue: Pigment(65, 105, 225, alpha: 1.0)
+ case .saddleBrown: Pigment(139, 69, 19, alpha: 1.0)
+ case .salmon: Pigment(250, 128, 114, alpha: 1.0)
+ case .sandyBrown: Pigment(244, 164, 96, alpha: 1.0)
+ case .seagreen: Pigment(46, 139, 87, alpha: 1.0)
+ case .seashell: Pigment(255, 245, 238, alpha: 1.0)
+ case .sienna: Pigment(160, 82, 45, alpha: 1.0)
+ case .silver: Pigment(192, 192, 192, alpha: 1.0)
+ case .skyBlue: Pigment(135, 206, 235, alpha: 1.0)
+ case .slateBlue: Pigment(106, 90, 205, alpha: 1.0)
+ case .slateGray: Pigment(112, 128, 144, alpha: 1.0)
+ case .slateGrey: Pigment(112, 128, 144, alpha: 1.0)
+ case .snow: Pigment(255, 250, 250, alpha: 1.0)
+ case .springGreen: Pigment(0, 255, 127, alpha: 1.0)
+ case .steelBlue: Pigment(70, 130, 180, alpha: 1.0)
+ case .tan: Pigment(210, 180, 140, alpha: 1.0)
+ case .teal: Pigment(0, 128, 128, alpha: 1.0)
+ case .thistle: Pigment(216, 191, 216, alpha: 1.0)
+ case .tomato: Pigment(255, 99, 71, alpha: 1.0)
+ case .turquoise: Pigment(64, 224, 208, alpha: 1.0)
+ case .violet: Pigment(238, 130, 238, alpha: 1.0)
+ case .wheat: Pigment(245, 222, 179, alpha: 1.0)
+ case .white: Pigment(255, 255, 255, alpha: 1.0)
+ case .whitesmoke: Pigment(245, 245, 245, alpha: 1.0)
+ case .yellow: Pigment(255, 255, 0, alpha: 1.0)
+ case .yellowGreen: Pigment(154, 205, 50, alpha: 1.0)
+ }
+ }
+ }
+
+ init(
+ _ name: Name,
+ @Clamping(0 ... 1) alpha: Double = 1.0
+ ) {
+ red = Double(name.rgb.red) / 255.0
+ green = Double(name.rgb.green) / 255.0
+ blue = Double(name.rgb.blue) / 255.0
+ self.alpha = alpha
+ }
+}
diff --git a/third-party/SwiftColor/Sources/Pigment+String.swift b/third-party/SwiftColor/Sources/Pigment+String.swift
new file mode 100644
index 0000000000..22c7b01124
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment+String.swift
@@ -0,0 +1,76 @@
+import Foundation
+
+public extension Pigment {
+ init(_ value: String, alpha: Double = 1.0) {
+ if let keyword = Name.allCases.first(where: { $0.rawValue.caseInsensitiveCompare(value) == .orderedSame }) {
+ red = keyword.pigment.red
+ green = keyword.pigment.green
+ blue = keyword.pigment.blue
+ self.alpha = alpha
+ return
+ }
+
+ if ExtendedKeyword.allCases.contains(where: { $0.rawValue.caseInsensitiveCompare(value) == .orderedSame }) {
+ red = 1.0
+ green = 1.0
+ blue = 1.0
+ self.alpha = alpha
+ return
+ }
+
+ var hex = value
+ if hex.hasPrefix("#") {
+ hex = String(hex.dropFirst())
+ }
+
+ guard let hexValue = Int(hex, radix: 16) else {
+ red = 1.0
+ green = 1.0
+ blue = 1.0
+ self.alpha = alpha
+ return
+ }
+
+ switch hex.count {
+ case 3:
+ let values = Self.hex3(hex: hexValue)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ case 4:
+ let values = Self.hex4(hex: hexValue)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ case 6:
+ let values = Self.hex6(hex: hexValue, alpha: alpha)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ #if !os(watchOS)
+ case 8:
+ let values = Self.hex8(hex: hexValue)
+ red = values.red
+ green = values.green
+ blue = values.blue
+ self.alpha = values.alpha
+ #endif
+ default:
+ red = 1.0
+ green = 1.0
+ blue = 1.0
+ self.alpha = alpha
+ }
+ }
+}
+
+private extension Pigment {
+ enum ExtendedKeyword: String, CaseIterable {
+ case none
+ case clear
+ case transparent
+ }
+}
diff --git a/third-party/SwiftColor/Sources/Pigment+SwiftUI.swift b/third-party/SwiftColor/Sources/Pigment+SwiftUI.swift
new file mode 100644
index 0000000000..d07aa1aec1
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment+SwiftUI.swift
@@ -0,0 +1,9 @@
+#if canImport(SwiftUI)
+import SwiftUI
+
+public extension Pigment {
+ var color: Color {
+ Color(red: red, green: green, blue: blue, opacity: alpha)
+ }
+}
+#endif
diff --git a/third-party/SwiftColor/Sources/Pigment+UIKit.swift b/third-party/SwiftColor/Sources/Pigment+UIKit.swift
new file mode 100644
index 0000000000..8c96c2cc7e
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment+UIKit.swift
@@ -0,0 +1,37 @@
+import Foundation
+#if canImport(UIKit)
+import UIKit
+
+public extension Pigment {
+ init(_ color: UIColor) {
+ var redComponent: CGFloat = 1.0
+ var greenComponent: CGFloat = 1.0
+ var blueComponent: CGFloat = 1.0
+ var alphaComponent: CGFloat = 1.0
+
+ guard color.getRed(&redComponent, green: &greenComponent, blue: &blueComponent, alpha: &alphaComponent) else {
+ // TODO: Fail Initializer? Default Colors?
+ red = redComponent
+ green = greenComponent
+ blue = blueComponent
+ alpha = alphaComponent
+ return
+ }
+
+ red = redComponent
+ green = greenComponent
+ blue = blueComponent
+ alpha = alphaComponent
+ }
+
+ var uiColor: UIColor {
+ UIColor(red: red, green: green, blue: blue, alpha: alpha)
+ }
+}
+
+public extension UIColor {
+ var pigment: Pigment {
+ Pigment(self)
+ }
+}
+#endif
diff --git a/third-party/SwiftColor/Sources/Pigment.swift b/third-party/SwiftColor/Sources/Pigment.swift
new file mode 100644
index 0000000000..3db75b50bb
--- /dev/null
+++ b/third-party/SwiftColor/Sources/Pigment.swift
@@ -0,0 +1,53 @@
+/// A platform agnostic representation of Color
+///
+/// The components - red, green, blue, & alpha - are maintained as a floating-point representation.
+/// Each value can range from 0.0 to 1.0 (e.g. 0 to 100 percent).
+///
+/// 'Pure White' is represented by values all equal to **1.0**.
+public struct Pigment: Sendable {
+
+ public let colorSpace: ColorSpace = .rgba
+ public let red: Double
+ public let green: Double
+ public let blue: Double
+ public let alpha: Double
+
+ public init(
+ @Clamping(0 ... 1) red: Double = 1.0,
+ @Clamping(0 ... 1) green: Double = 1.0,
+ @Clamping(0 ... 1) blue: Double = 1.0,
+ @Clamping(0 ... 1) alpha: Double = 1.0
+ ) {
+ self.red = red
+ self.green = green
+ self.blue = blue
+ self.alpha = alpha
+ }
+}
+
+extension Pigment: CustomStringConvertible {
+ public var description: String {
+ String(format: "Pigment(red: %.4f, green: %.4f, blue: %.4f, alpha: %.2f)", red, green, blue, alpha)
+ }
+}
+
+extension Pigment: Equatable {
+ public static func == (lhs: Pigment, rhs: Pigment) -> Bool {
+ guard lhs.red == rhs.red else {
+ return false
+ }
+ guard lhs.green == rhs.green else {
+ return false
+ }
+ guard lhs.blue == rhs.blue else {
+ return false
+ }
+ guard lhs.alpha == rhs.alpha else {
+ return false
+ }
+ guard lhs.colorSpace == rhs.colorSpace else {
+ return false
+ }
+ return true
+ }
+}
diff --git a/third-party/SwiftSVG/BUILD b/third-party/SwiftSVG/BUILD
new file mode 100644
index 0000000000..565ad88ec4
--- /dev/null
+++ b/third-party/SwiftSVG/BUILD
@@ -0,0 +1,19 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "SwiftSVG",
+ module_name = "SwiftSVG",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//third-party/XMLCoder",
+ "//third-party/Swift2D",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/third-party/SwiftSVG/Sources/Circle.swift b/third-party/SwiftSVG/Sources/Circle.swift
new file mode 100644
index 0000000000..d42c8b9c73
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Circle.swift
@@ -0,0 +1,93 @@
+import Swift2D
+import XMLCoder
+
+/// Basic shape, used to draw circles based on a center point and a radius.
+///
+/// The arc of a ‘circle’ element begins at the "3 o'clock" point on the radius and progresses towards the
+/// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform
+/// in the same manner as the geometry of the element.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle)
+/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#CircleElement)
+public struct Circle: Element {
+
+ /// The x-axis coordinate of the center of the circle.
+ public var x: Double = 0.0
+ /// The y-axis coordinate of the center of the circle.
+ public var y: Double = 0.0
+ /// The radius of the circle.
+ public var r: Double = 0.0
+
+ // MARK: CoreAttributes
+
+ public var id: String?
+
+ // MARK: PresentationAttributes
+
+ public var fillColor: String?
+ public var fillOpacity: Double?
+ public var fillRule: Fill.Rule?
+ public var strokeColor: String?
+ public var strokeWidth: Double?
+ public var strokeOpacity: Double?
+ public var strokeLineCap: Stroke.LineCap?
+ public var strokeLineJoin: Stroke.LineJoin?
+ public var strokeMiterLimit: Double?
+ public var transform: String?
+
+ // MARK: StylingAttributes
+
+ public var style: String?
+
+ enum CodingKeys: String, CodingKey {
+ case x = "cx"
+ case y = "cy"
+ case r
+ case id
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+ case style
+ }
+
+ public init() {}
+
+ public init(x: Double, y: Double, r: Double) {
+ self.x = x
+ self.y = y
+ self.r = r
+ }
+}
+
+extension Circle: CustomStringConvertible {
+ public var description: String {
+ let desc = ""
+ }
+}
+
+extension Circle: DirectionalCommandRepresentable {
+ public func commands(clockwise: Bool) throws -> [Path.Command] {
+ EllipseProcessor(circle: self).commands(clockwise: clockwise)
+ }
+}
+
+extension Circle: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ .attribute
+ }
+}
+
+extension Circle: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ .attribute
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/CommandRepresentable.swift b/third-party/SwiftSVG/Sources/CommandRepresentable.swift
new file mode 100644
index 0000000000..f6bb79f05d
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/CommandRepresentable.swift
@@ -0,0 +1,15 @@
+/// Elements conforming to `CommandRepresentable` can be expressed in the form of `Path.Command`s.
+public protocol CommandRepresentable {
+ func commands() throws -> [Path.Command]
+}
+
+public protocol DirectionalCommandRepresentable: CommandRepresentable {
+ func commands(clockwise: Bool) throws -> [Path.Command]
+}
+
+public extension DirectionalCommandRepresentable {
+ /// Defaults to anti/counter-clockwise commands.
+ func commands() throws -> [Path.Command] {
+ try commands(clockwise: false)
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Container.swift b/third-party/SwiftSVG/Sources/Container.swift
new file mode 100644
index 0000000000..c90f0ea33a
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Container.swift
@@ -0,0 +1,58 @@
+public protocol Container {
+ var circles: [Circle]? { get set }
+ var ellipses: [Ellipse]? { get set }
+ var groups: [Group]? { get set }
+ var lines: [Line]? { get set }
+ var paths: [Path]? { get set }
+ var polygons: [Polygon]? { get set }
+ var polylines: [Polyline]? { get set }
+ var rectangles: [Rectangle]? { get set }
+ var texts: [Text]? { get set }
+}
+
+enum ContainerKeys: String, CodingKey {
+ case circles = "circle"
+ case ellipses = "ellipse"
+ case groups = "g"
+ case lines = "line"
+ case paths = "path"
+ case polylines = "polyline"
+ case polygons = "polygon"
+ case rectangles = "rect"
+ case texts = "text"
+}
+
+public extension Container {
+ var containerDescription: String {
+ var contents: String = ""
+
+ let circles = circles?.compactMap(\.description) ?? []
+ circles.forEach { contents.append("\n\($0)") }
+
+ let ellipses = ellipses?.compactMap(\.description) ?? []
+ ellipses.forEach { contents.append("\n\($0)") }
+
+ let groups = groups?.compactMap(\.description) ?? []
+ groups.forEach { contents.append("\n\($0)") }
+
+ let lines = lines?.compactMap(\.description) ?? []
+ lines.forEach { contents.append("\n\($0)") }
+
+ let paths = paths?.compactMap(\.description) ?? []
+ paths.forEach { contents.append("\n\($0)") }
+
+ let polylines = polylines?.compactMap(\.description) ?? []
+ polylines.forEach { contents.append("\n\($0)") }
+
+ let polygons = polygons?.compactMap(\.description) ?? []
+ polygons.forEach { contents.append("\n\($0)") }
+
+ let rectangles = rectangles?.compactMap(\.description) ?? []
+ rectangles.forEach { contents.append("\n\($0)") }
+
+ let texts = texts?.compactMap(\.description) ?? []
+ texts.forEach { contents.append("\n\($0)") }
+
+ return contents
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/CoreAttributes.swift b/third-party/SwiftSVG/Sources/CoreAttributes.swift
new file mode 100644
index 0000000000..df93043baa
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/CoreAttributes.swift
@@ -0,0 +1,17 @@
+public protocol CoreAttributes {
+ var id: String? { get set }
+}
+
+enum CoreAttributesKeys: String, CodingKey {
+ case id
+}
+
+public extension CoreAttributes {
+ var coreDescription: String {
+ if let id {
+ "\(CoreAttributesKeys.id.rawValue)=\"\(id)\""
+ } else {
+ ""
+ }
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Element.swift b/third-party/SwiftSVG/Sources/Element.swift
new file mode 100644
index 0000000000..d9a612ef8e
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Element.swift
@@ -0,0 +1,46 @@
+public protocol Element: CoreAttributes, PresentationAttributes, StylingAttributes {}
+
+public extension Element {
+ var attributeDescription: String {
+ var components: [String] = []
+
+ if !coreDescription.isEmpty {
+ components.append(coreDescription)
+ }
+ if !presentationDescription.isEmpty {
+ components.append(presentationDescription)
+ }
+ if !stylingDescription.isEmpty {
+ components.append(stylingDescription)
+ }
+
+ return components.joined(separator: " ")
+ }
+}
+
+public extension CommandRepresentable where Self: Element {
+ /// When a `Path` is accessed on an element, the path that is returned should have the supplied transformations
+ /// applied.
+ ///
+ /// For instance, if
+ /// * a `Path.data` contains relative elements,
+ /// * and `transformations` contains a `.translate`
+ ///
+ /// Than the path created will not only use 'absolute' instructions, but those instructions will be modified to
+ /// include the required transformation.
+ func path(applying transformations: [Transformation] = []) throws -> Path {
+ var _transformations = transformations
+ _transformations.append(contentsOf: self.transformations)
+
+ let commands = try commands().map { $0.applying(transformations: _transformations) }
+
+ var path = Path(commands: commands)
+ path.fillColor = fillColor
+ path.fillOpacity = fillOpacity
+ path.strokeColor = strokeColor
+ path.strokeOpacity = strokeOpacity
+ path.strokeWidth = strokeWidth
+
+ return path
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Ellipse.swift b/third-party/SwiftSVG/Sources/Ellipse.swift
new file mode 100644
index 0000000000..bbabf36e61
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Ellipse.swift
@@ -0,0 +1,93 @@
+import Swift2D
+import XMLCoder
+
+/// SVG basic shape, used to create ellipses based on a center coordinate, and both their x and y radius.
+///
+/// The arc of an ‘ellipse’ element begins at the "3 o'clock" point on the radius and progresses towards the
+/// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform in the same
+/// manner as the geometry of the element.
+public struct Ellipse: Element {
+
+ /// The x position of the ellipse.
+ public var x: Double = 0.0
+ /// The y position of the ellipse.
+ public var y: Double = 0.0
+ /// The radius of the ellipse on the x axis.
+ public var rx: Double = 0.0
+ /// The radius of the ellipse on the y axis.
+ public var ry: Double = 0.0
+
+ // MARK: CoreAttributes
+
+ public var id: String?
+
+ // MARK: PresentationAttributes
+
+ public var fillColor: String?
+ public var fillOpacity: Double?
+ public var fillRule: Fill.Rule?
+ public var strokeColor: String?
+ public var strokeWidth: Double?
+ public var strokeOpacity: Double?
+ public var strokeLineCap: Stroke.LineCap?
+ public var strokeLineJoin: Stroke.LineJoin?
+ public var strokeMiterLimit: Double?
+ public var transform: String?
+
+ // MARK: StylingAttributes
+
+ public var style: String?
+
+ enum CodingKeys: String, CodingKey {
+ case x = "cx"
+ case y = "cy"
+ case rx
+ case ry
+ case id
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+ case style
+ }
+
+ public init() {}
+
+ public init(x: Double, y: Double, rx: Double, ry: Double) {
+ self.x = x
+ self.y = y
+ self.rx = rx
+ self.ry = ry
+ }
+}
+
+extension Ellipse: CustomStringConvertible {
+ public var description: String {
+ let desc = ""
+ }
+}
+
+extension Ellipse: DirectionalCommandRepresentable {
+ public func commands(clockwise: Bool) throws -> [Path.Command] {
+ EllipseProcessor(ellipse: self).commands(clockwise: clockwise)
+ }
+}
+
+extension Ellipse: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ .attribute
+ }
+}
+
+extension Ellipse: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ .attribute
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Extensions/Point+SwiftSVG.swift b/third-party/SwiftSVG/Sources/Extensions/Point+SwiftSVG.swift
new file mode 100644
index 0000000000..0c789bcae3
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Extensions/Point+SwiftSVG.swift
@@ -0,0 +1,37 @@
+import Swift2D
+
+extension Point {
+ static var nan: Point {
+ Point(x: Double.nan, y: Double.nan)
+ }
+
+ var hasNaN: Bool {
+ x.isNaN || y.isNaN
+ }
+
+ /// Returns a copy of the instance with the **x** value replaced with the provided value.
+ func with(x value: Double) -> Point {
+ Point(x: value, y: y)
+ }
+
+ /// Returns a copy of the instance with the **y** value replaced with the provided value.
+ func with(y value: Double) -> Point {
+ Point(x: x, y: value)
+ }
+
+ /// Adjusts the **x** value by the provided amount.
+ ///
+ /// This will explicitly check for `.isNaN`, and if encountered, will simply
+ /// use the provided value.
+ func adjusting(x value: Double) -> Point {
+ (x.isNaN) ? with(x: value) : with(x: x + value)
+ }
+
+ /// Adjusts the **y** value by the provided amount.
+ ///
+ /// This will explicitly check for `.isNaN`, and if encountered, will simply
+ /// use the provided value.
+ func adjusting(y value: Double) -> Point {
+ (y.isNaN) ? with(y: value) : with(y: y + value)
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Fill.swift b/third-party/SwiftSVG/Sources/Fill.swift
new file mode 100644
index 0000000000..608192aa29
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Fill.swift
@@ -0,0 +1,44 @@
+import Swift2D
+
+public struct Fill {
+
+ public var color: String?
+ public var opacity: Double?
+ public var rule: Rule = .nonZero
+
+ public init() {}
+
+ /// Presentation attribute defining the algorithm to use to determine the inside part of a shape.
+ ///
+ /// The default `Rule` is `.nonzero`.
+ public enum Rule: String, Sendable, Codable, CaseIterable {
+ /// The value evenodd determines the "insideness" of a point in the shape by drawing a ray from that point to
+ /// infinity in any direction and counting the number of path segments from the given shape that the ray
+ /// crosses. If this number is odd, the point is inside; if even, the point is outside.
+ case evenOdd = "evenodd"
+ /// The value nonzero determines the "insideness" of a point in the shape by drawing a ray from that point to
+ /// infinity in any direction, and then examining the places where a segment of the shape crosses the ray.
+ /// Starting with a count of zero, add one each time a path segment crosses the ray from left to right and
+ /// subtract one each time a path segment crosses the ray from right to left. After counting the crossings, if
+ /// the result is zero then the point is outside the path. Otherwise, it is inside.
+ case nonZero = "nonzero"
+
+ public init(from decoder: any Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let rawValue = try container.decode(String.self)
+ guard let rule = Rule(rawValue: rawValue) else {
+ print("Attempts to decode Fill.Rule with rawValue: '\(rawValue)'")
+ self = .nonZero
+ return
+ }
+
+ self = rule
+ }
+ }
+}
+
+extension Fill.Rule: CustomStringConvertible {
+ public var description: String {
+ rawValue
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Group.swift b/third-party/SwiftSVG/Sources/Group.swift
new file mode 100644
index 0000000000..c95d2d0ad5
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Group.swift
@@ -0,0 +1,152 @@
+import XMLCoder
+
+/// A container used to group other SVG elements.
+///
+/// Grouping constructs, when used in conjunction with the ‘desc’ and ‘title’ elements, provide information
+/// about document structure and semantics.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g)
+/// | [W3](https://www.w3.org/TR/SVG11/struct.html#Groups)
+public struct Group: Container, Element {
+
+ // Container
+ public var circles: [Circle]?
+ public var ellipses: [Ellipse]?
+ public var groups: [Group]?
+ public var lines: [Line]?
+ public var paths: [Path]?
+ public var polygons: [Polygon]?
+ public var polylines: [Polyline]?
+ public var rectangles: [Rectangle]?
+ public var texts: [Text]?
+
+ // MARK: CoreAttributes
+
+ public var id: String?
+ public var title: String?
+ public var desc: String?
+
+ // MARK: PresentationAttributes
+
+ public var fillColor: String?
+ public var fillOpacity: Double?
+ public var fillRule: Fill.Rule?
+ public var strokeColor: String?
+ public var strokeWidth: Double?
+ public var strokeOpacity: Double?
+ public var strokeLineCap: Stroke.LineCap?
+ public var strokeLineJoin: Stroke.LineJoin?
+ public var strokeMiterLimit: Double?
+ public var transform: String?
+
+ // MARK: StylingAttributes
+
+ public var style: String?
+
+ enum CodingKeys: String, CodingKey {
+ case circles = "circle"
+ case ellipses = "ellipse"
+ case groups = "g"
+ case lines = "line"
+ case paths = "path"
+ case polylines = "polyline"
+ case polygons = "polygon"
+ case rectangles = "rect"
+ case texts = "text"
+ case id
+ case title
+ case desc
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+ case style
+ }
+
+ public init() {}
+
+ /// A representation of all the sub-`Path`s in the `Group`.
+ public func subpaths(applying transformations: [Transformation] = []) throws -> [Path] {
+ var _transformations = transformations
+ _transformations.append(contentsOf: self.transformations)
+
+ var output: [Path] = []
+
+ if let circles {
+ try output.append(contentsOf: circles.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let ellipses {
+ try output.append(contentsOf: ellipses.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let rectangles {
+ try output.append(contentsOf: rectangles.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let polygons {
+ try output.append(contentsOf: polygons.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let polylines {
+ try output.append(contentsOf: polylines.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let paths {
+ try output.append(contentsOf: paths.map { try $0.path(applying: _transformations) })
+ }
+
+ if let groups {
+ try groups.forEach {
+ try output.append(contentsOf: $0.subpaths(applying: _transformations))
+ }
+ }
+
+ return output
+ }
+}
+
+extension Group: CustomStringConvertible {
+ public var description: String {
+ var contents: String = ""
+
+ if let title {
+ contents.append("\n\(title)")
+ }
+
+ if let desc {
+ contents.append("\n\(desc)")
+ }
+
+ contents.append(containerDescription)
+
+ return "\(contents)\n"
+ }
+}
+
+extension Group: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ if let _ = ContainerKeys(stringValue: key.stringValue) {
+ return .element
+ }
+
+ return .attribute
+ }
+}
+
+extension Group: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ if let _ = ContainerKeys(stringValue: key.stringValue) {
+ return .element
+ }
+
+ return .attribute
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Internal/EllipseProcessor.swift b/third-party/SwiftSVG/Sources/Internal/EllipseProcessor.swift
new file mode 100644
index 0000000000..409c2a7163
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Internal/EllipseProcessor.swift
@@ -0,0 +1,88 @@
+import Foundation
+import Swift2D
+
+struct EllipseProcessor {
+
+ let x: Double
+ let y: Double
+ let rx: Double
+ let ry: Double
+
+ /// The _optimal_ offset for control points when representing a
+ /// circle/ellipse as 4 bezier curves.
+ ///
+ /// [Stack Overflow](https://stackoverflow.com/questions/1734745/how-to-create-circle-with-bézier-curves)
+ static func controlPointOffset(_ radius: Double) -> Double {
+ (Double(4.0 / 3.0) * tan(Double.pi / 8.0)) * radius
+ }
+
+ init(ellipse: Ellipse) {
+ x = ellipse.x
+ y = ellipse.y
+ rx = ellipse.rx
+ ry = ellipse.ry
+ }
+
+ init(circle: Circle) {
+ x = circle.x
+ y = circle.y
+ rx = circle.r
+ ry = circle.r
+ }
+
+ func commands(clockwise: Bool) -> [Path.Command] {
+ var commands: [Path.Command] = []
+
+ let xOffset = Self.controlPointOffset(rx)
+ let yOffset = Self.controlPointOffset(ry)
+
+ let zero = Point(x: x + rx, y: y)
+ let ninety = Point(x: x, y: y - ry)
+ let oneEighty = Point(x: x - rx, y: y)
+ let twoSeventy = Point(x: x, y: y + ry)
+
+ var cp1: Point = .zero
+ var cp2: Point = .zero
+
+ // Starting at degree 0 (the right most point)
+ commands.append(.moveTo(point: zero))
+
+ if clockwise {
+ cp1 = Point(x: zero.x, y: zero.y + yOffset)
+ cp2 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy))
+
+ cp1 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y)
+ cp2 = Point(x: oneEighty.x, y: oneEighty.y + yOffset)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty))
+
+ cp1 = Point(x: oneEighty.x, y: oneEighty.y - yOffset)
+ cp2 = Point(x: ninety.x - xOffset, y: ninety.y)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety))
+
+ cp1 = Point(x: ninety.x + xOffset, y: ninety.y)
+ cp2 = Point(x: zero.x, y: zero.y - yOffset)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero))
+ } else {
+ cp1 = Point(x: zero.x, y: zero.y - yOffset)
+ cp2 = Point(x: ninety.x + xOffset, y: ninety.y)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety))
+
+ cp1 = Point(x: ninety.x - xOffset, y: ninety.y)
+ cp2 = Point(x: oneEighty.x, y: oneEighty.y - yOffset)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty))
+
+ cp1 = Point(x: oneEighty.x, y: oneEighty.y + yOffset)
+ cp2 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy))
+
+ cp1 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y)
+ cp2 = Point(x: zero.x, y: zero.y + yOffset)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero))
+ }
+
+ commands.append(.closePath)
+
+ return commands
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Internal/PathProcessor.swift b/third-party/SwiftSVG/Sources/Internal/PathProcessor.swift
new file mode 100644
index 0000000000..25c808c4d7
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Internal/PathProcessor.swift
@@ -0,0 +1,12 @@
+import Foundation
+
+struct PathProcessor {
+
+ let data: String
+
+ func commands() throws -> [Path.Command] {
+ let parser = Path.ComponentParser()
+ let components = try Path.Component.components(from: data)
+ return try parser.parse(components)
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Internal/PolygonProcressor.swift b/third-party/SwiftSVG/Sources/Internal/PolygonProcressor.swift
new file mode 100644
index 0000000000..d05ab1a408
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Internal/PolygonProcressor.swift
@@ -0,0 +1,52 @@
+import Foundation
+import Swift2D
+
+struct PolygonProcessor {
+
+ let points: String
+
+ func commands() throws -> [Path.Command] {
+ let pairs = points.components(separatedBy: " ")
+ let components = pairs.flatMap { $0.components(separatedBy: ",") }
+ guard components.count > 0 else {
+ return []
+ }
+
+ guard components.count % 2 == 0 else {
+ // An odd number of components means that parsing probably failed
+ return []
+ }
+
+ var commands: [Path.Command] = []
+
+ var firstValue: Bool = true
+ for (idx, component) in components.enumerated() {
+ guard let _value = Double(component) else {
+ return commands
+ }
+
+ let value = Double(_value)
+
+ if firstValue {
+ if idx == 0 {
+ commands.append(.moveTo(point: Point(x: value, y: .nan)))
+ } else {
+ commands.append(.lineTo(point: Point(x: value, y: .nan)))
+ }
+ firstValue = false
+ } else {
+ let count = commands.count
+ guard let modified = try? commands.last?.adjustingArgument(at: 1, by: value) else {
+ return commands
+ }
+
+ commands[count - 1] = modified
+ firstValue = true
+ }
+ }
+
+ commands.append(.closePath)
+
+ return commands
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Internal/PolylineProcessor.swift b/third-party/SwiftSVG/Sources/Internal/PolylineProcessor.swift
new file mode 100644
index 0000000000..a909c4ed3f
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Internal/PolylineProcessor.swift
@@ -0,0 +1,54 @@
+import Foundation
+import Swift2D
+
+struct PolylineProcessor {
+
+ let points: String
+
+ func commands() throws -> [Path.Command] {
+ let pairs = points.components(separatedBy: " ")
+ let components = pairs.flatMap { $0.components(separatedBy: ",") }
+ let values = components.compactMap { Double($0) }.map { Double($0) }
+
+ guard values.count > 2 else {
+ // More than just a starting point is required.
+ return []
+ }
+
+ guard values.count % 2 == 0 else {
+ // An odd number of components means that parsing probably failed
+ return []
+ }
+
+ var commands: [Path.Command] = []
+
+ let move = values.prefix(upTo: 2)
+ let segments = values.suffix(from: 2)
+
+ commands.append(.moveTo(point: Point(x: move[0], y: move[1])))
+
+ var _value: Double = .nan
+ for value in segments {
+ if _value.isNaN {
+ _value = value
+ } else {
+ commands.append(.lineTo(point: Point(x: _value, y: value)))
+ _value = .nan
+ }
+ }
+
+ let reversedSegments = segments.dropLast(2).reversed()
+ for value in reversedSegments {
+ if _value.isNaN {
+ _value = value
+ } else {
+ commands.append(.lineTo(point: Point(x: _value, y: value)))
+ _value = .nan
+ }
+ }
+
+ commands.append(.closePath)
+
+ return commands
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Internal/RectangleProcessor.swift b/third-party/SwiftSVG/Sources/Internal/RectangleProcessor.swift
new file mode 100644
index 0000000000..2b93440834
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Internal/RectangleProcessor.swift
@@ -0,0 +1,175 @@
+import Swift2D
+
+struct RectangleProcessor {
+
+ let rectangle: Rectangle
+
+ func commands(clockwise: Bool) -> [Path.Command] {
+ var rx = rectangle.rx
+ var ry = rectangle.ry
+
+ if let _rx = rx, _rx > (rectangle.width / 2.0) {
+ rx = rectangle.width / 2.0
+ }
+
+ if let _ry = ry, _ry > (rectangle.height / 2.0) {
+ ry = rectangle.height / 2.0
+ }
+
+ var commands: [Path.Command] = []
+
+ switch (rx, ry) {
+ case (.some(let radiusX), .some(let radiusY)) where radiusX != radiusY:
+ // Use Cubic Bezier Curve to form rounded corners
+ // TODO: Verify that the control points are right
+
+ var cp1: Point = .zero
+ var cp2: Point = .zero
+ var point: Point = Point(x: rectangle.x + radiusX, y: rectangle.y)
+
+ commands.append(.moveTo(point: point))
+
+ if clockwise {
+ point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y)
+ commands.append(.lineTo(point: point))
+
+ cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
+ cp2 = cp1
+ point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
+
+ point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY)
+ commands.append(.lineTo(point: point))
+
+ cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
+ cp2 = cp1
+ point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
+
+ point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height)
+ commands.append(.lineTo(point: point))
+
+ cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
+ cp2 = cp1
+ point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
+
+ point = .init(x: rectangle.x, y: rectangle.y + radiusY)
+ commands.append(.lineTo(point: point))
+
+ cp1 = .init(x: rectangle.x, y: rectangle.y)
+ cp2 = cp1
+ point = .init(x: rectangle.x + radiusX, y: rectangle.y)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
+ } else {
+ cp1 = .init(x: rectangle.x, y: rectangle.y)
+ cp2 = cp1
+ point = .init(x: rectangle.x, y: rectangle.y + radiusY)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
+
+ point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY)
+ commands.append(.lineTo(point: point))
+
+ cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
+ cp2 = cp1
+ point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
+
+ point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height)
+ commands.append(.lineTo(point: point))
+
+ cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
+ cp2 = cp1
+ point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
+
+ point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY)
+ commands.append(.lineTo(point: point))
+
+ cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
+ cp2 = cp1
+ point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y)
+ commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
+ }
+ case (.some(let radius), .none), (.none, .some(let radius)), (.some(let radius), _):
+ // use Quadratic Bezier Curve to form rounded corners
+
+ var cp: Point = .zero
+ var point: Point = Point(x: rectangle.x + radius, y: rectangle.y)
+
+ commands.append(.moveTo(point: point))
+
+ if clockwise {
+ point = .init(x: (rectangle.x + rectangle.width) - radius, y: rectangle.y)
+ commands.append(.lineTo(point: point))
+
+ cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
+ point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius)
+ commands.append(.quadraticBezierCurve(cp: cp, point: point))
+
+ point = .init(x: rectangle.x + rectangle.width, y: (rectangle.y + rectangle.height) - radius)
+ commands.append(.lineTo(point: point))
+
+ cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
+ point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height)
+ commands.append(.quadraticBezierCurve(cp: cp, point: point))
+
+ point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height)
+ commands.append(.lineTo(point: point))
+
+ cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
+ point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius)
+ commands.append(.quadraticBezierCurve(cp: cp, point: point))
+
+ point = .init(x: rectangle.x, y: rectangle.y + radius)
+ commands.append(.lineTo(point: point))
+
+ cp = .init(x: rectangle.x, y: rectangle.y)
+ point = .init(x: rectangle.x + radius, y: rectangle.y)
+ commands.append(.quadraticBezierCurve(cp: cp, point: point))
+ } else {
+ cp = .init(x: rectangle.x, y: rectangle.y)
+ point = .init(x: rectangle.x, y: rectangle.y + radius)
+ commands.append(.quadraticBezierCurve(cp: cp, point: point))
+
+ point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius)
+ commands.append(.lineTo(point: point))
+
+ cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
+ point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height)
+ commands.append(.quadraticBezierCurve(cp: cp, point: point))
+
+ point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height)
+ commands.append(.lineTo(point: point))
+
+ cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
+ point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radius)
+ commands.append(.quadraticBezierCurve(cp: cp, point: point))
+
+ point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius)
+ commands.append(.lineTo(point: point))
+
+ cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
+ point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y)
+ commands.append(.quadraticBezierCurve(cp: cp, point: point))
+ }
+ case (.none, .none):
+ // draw three line segments.
+ commands.append(.moveTo(point: Point(x: rectangle.x, y: rectangle.y)))
+
+ if clockwise {
+ commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y)))
+ commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)))
+ commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height)))
+ } else {
+ commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height)))
+ commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)))
+ commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y)))
+ }
+ }
+
+ commands.append(.closePath)
+
+ return commands
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Line.swift b/third-party/SwiftSVG/Sources/Line.swift
new file mode 100644
index 0000000000..08ee77488f
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Line.swift
@@ -0,0 +1,98 @@
+import Swift2D
+import XMLCoder
+
+/// SVG basic shape used to create a line connecting two points.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
+/// | [W3](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
+public struct Line: Element {
+
+ /// Defines the x-axis coordinate of the line starting point.
+ public var x1: Double = 0.0
+ /// Defines the x-axis coordinate of the line ending point.
+ public var y1: Double = 0.0
+ /// Defines the y-axis coordinate of the line starting point.
+ public var x2: Double = 0.0
+ /// Defines the y-axis coordinate of the line ending point.
+ public var y2: Double = 0.0
+
+ // MARK: CoreAttributes
+
+ public var id: String?
+
+ // MARK: PresentationAttributes
+
+ public var fillColor: String?
+ public var fillOpacity: Double?
+ public var fillRule: Fill.Rule?
+ public var strokeColor: String?
+ public var strokeWidth: Double?
+ public var strokeOpacity: Double?
+ public var strokeLineCap: Stroke.LineCap?
+ public var strokeLineJoin: Stroke.LineJoin?
+ public var strokeMiterLimit: Double?
+ public var transform: String?
+
+ // MARK: StylingAttributes
+
+ public var style: String?
+
+ enum CodingKeys: String, CodingKey {
+ case x1
+ case y1
+ case x2
+ case y2
+ case id
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+ case style
+ }
+
+ public init() {}
+
+ public init(x1: Double, y1: Double, x2: Double, y2: Double) {
+ self.x1 = x1
+ self.y1 = y1
+ self.x2 = x2
+ self.y2 = y2
+ }
+}
+
+extension Line: CommandRepresentable {
+ public func commands() throws -> [Path.Command] {
+ [
+ .moveTo(point: Point(x: x1, y: y1)),
+ .lineTo(point: Point(x: x2, y: y2)),
+ .lineTo(point: Point(x: x1, y: y1)),
+ .closePath,
+ ]
+ }
+}
+
+extension Line: CustomStringConvertible {
+ public var description: String {
+ let desc = ""
+ }
+}
+
+extension Line: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ .attribute
+ }
+}
+
+extension Line: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ .attribute
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Path.Command.swift b/third-party/SwiftSVG/Sources/Path.Command.swift
new file mode 100644
index 0000000000..e0fb49a729
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Path.Command.swift
@@ -0,0 +1,276 @@
+import Foundation
+import Swift2D
+
+public extension Path {
+ /// Path commands are instructions that define a path to be drawn.
+ ///
+ /// Each command is composed of a command letter and numbers that represent the command parameters.
+ enum Command: Equatable, Sendable, CustomStringConvertible {
+ /// Moves the current drawing point
+ case moveTo(point: Point)
+ /// Draw a straight line from the current point to the point provided
+ case lineTo(point: Point)
+ /// Draw a smooth curve using three points (+ origin)
+ case cubicBezierCurve(cp1: Point, cp2: Point, point: Point)
+ /// Draw a smooth curve using two points (+ origin)
+ case quadraticBezierCurve(cp: Point, point: Point)
+ /// Draw a curve defined as a portion of an ellipse
+ case ellipticalArcCurve(rx: Double, ry: Double, angle: Double, largeArc: Bool, clockwise: Bool, point: Point)
+ /// ClosePath instructions draw a straight line from the current position to the first point in the path.
+ case closePath
+
+ public enum Prefix: Character, CaseIterable {
+ case move = "M"
+ case relativeMove = "m"
+ case line = "L"
+ case relativeLine = "l"
+ case horizontalLine = "H"
+ case relativeHorizontalLine = "h"
+ case verticalLine = "V"
+ case relativeVerticalLine = "v"
+ case cubicBezierCurve = "C"
+ case relativeCubicBezierCurve = "c"
+ case smoothCubicBezierCurve = "S"
+ case relativeSmoothCubicBezierCurve = "s"
+ case quadraticBezierCurve = "Q"
+ case relativeQuadraticBezierCurve = "q"
+ case smoothQuadraticBezierCurve = "T"
+ case relativeSmoothQuadraticBezierCurve = "t"
+ case ellipticalArcCurve = "A"
+ case relativeEllipticalArcCurve = "a"
+ case close = "Z"
+ case relativeClose = "z"
+
+ public static var characterSet: CharacterSet {
+ CharacterSet(charactersIn: allCases.map { String($0.rawValue) }.joined())
+ }
+ }
+
+ public enum Coordinates {
+ case absolute
+ case relative
+ }
+
+ public enum Error: Swift.Error {
+ case message(String)
+ case invalidAdjustment(Path.Command)
+ case invalidArgumentPosition(Int, Path.Command)
+ case invalidRelativeCommand
+ }
+
+ public var description: String {
+ switch self {
+ case .moveTo(let point):
+ return "\(Prefix.move.rawValue)\(point.x),\(point.y)"
+ case .lineTo(let point):
+ return "\(Prefix.line.rawValue)\(point.x),\(point.y)"
+ case .cubicBezierCurve(let cp1, let cp2, let point):
+ return "\(Prefix.cubicBezierCurve.rawValue)\(cp1.x),\(cp1.y) \(cp2.x),\(cp2.y) \(point.x),\(point.y)"
+ case .quadraticBezierCurve(let cp, let point):
+ return "\(Prefix.quadraticBezierCurve.rawValue)\(cp.x),\(cp.y) \(point.x),\(point.y)"
+ case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
+ let la = largeArc ? 1 : 0
+ let cw = clockwise ? 1 : 0
+ return "\(Prefix.ellipticalArcCurve.rawValue)\(rx) \(ry) \(angle) \(la) \(cw) \(point.x) \(point.y)"
+ case .closePath:
+ return "\(Prefix.close.rawValue)"
+ }
+ }
+
+ /// The primary point that dictates the commands action.
+ public var point: Point {
+ switch self {
+ case .moveTo(let point): point
+ case .lineTo(let point): point
+ case .cubicBezierCurve(_, _, let point): point
+ case .quadraticBezierCurve(_, let point): point
+ case .ellipticalArcCurve(_, _, _, _, _, let point): point
+ case .closePath: .zero
+ }
+ }
+ }
+}
+
+public extension Path.Command {
+ /// Applies the provided `Transformation` to the instances values.
+ func applying(transformation: Transformation) -> Path.Command {
+ switch transformation {
+ case .translate(let x, let y):
+ switch self {
+ case .moveTo(let point):
+ let _point = point.adjusting(x: x).adjusting(y: y)
+ return .moveTo(point: _point)
+ case .lineTo(let point):
+ let _point = point.adjusting(x: x).adjusting(y: y)
+ return .lineTo(point: _point)
+ case .cubicBezierCurve(let cp1, let cp2, let point):
+ let _cp1 = cp1.adjusting(x: x).adjusting(y: y)
+ let _cp2 = cp2.adjusting(x: x).adjusting(y: y)
+ let _point = point.adjusting(x: x).adjusting(y: y)
+ return .cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point)
+ case .quadraticBezierCurve(let cp, let point):
+ let _cp = cp.adjusting(x: x).adjusting(y: y)
+ let _point = point.adjusting(x: x).adjusting(y: y)
+ return .quadraticBezierCurve(cp: _cp, point: _point)
+ case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
+ let _point = point.adjusting(x: x).adjusting(y: y)
+ return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: _point)
+ case .closePath:
+ return self
+ }
+ case .matrix:
+ // TODO: What should occur here?
+ return self
+ }
+ }
+
+ /// Applies multiple transformations in the order they are specified.
+ func applying(transformations: [Transformation]) -> Path.Command {
+ var command = self
+
+ for transformation in transformations {
+ command = command.applying(transformation: transformation)
+ }
+
+ return command
+ }
+}
+
+extension Path.Command {
+ /// Determines if all values are provided (i.e. !.isNaN)
+ var isComplete: Bool {
+ switch self {
+ case .moveTo(let point), .lineTo(let point):
+ !point.hasNaN
+ case .cubicBezierCurve(let cp1, let cp2, let point):
+ !cp1.hasNaN && !cp2.hasNaN && !point.hasNaN
+ case .quadraticBezierCurve(let cp, let point):
+ !cp.hasNaN && !point.hasNaN
+ case .ellipticalArcCurve(let rx, let ry, let angle, _, _, let point):
+ !rx.isNaN && !ry.isNaN && !angle.isNaN && !point.hasNaN
+ case .closePath:
+ true
+ }
+ }
+
+ /// The last control point used in drawing the path.
+ ///
+ /// Only valid for curves.
+ var lastControlPoint: Point? {
+ switch self {
+ case .cubicBezierCurve(_, let cp2, _):
+ cp2
+ case .quadraticBezierCurve(let cp, _):
+ cp
+ default:
+ nil
+ }
+ }
+
+ /// A mirror representation of `lastControlPoint`.
+ var lastControlPointMirror: Point? {
+ guard let cp = lastControlPoint else {
+ return nil
+ }
+
+ return Point(x: point.x + (point.x - cp.x), y: point.y + (point.y - cp.y))
+ }
+
+ /// The total number of argument values the command requires.
+ var arguments: Int {
+ switch self {
+ case .moveTo: 2
+ case .lineTo: 2
+ case .cubicBezierCurve: 6
+ case .quadraticBezierCurve: 4
+ case .ellipticalArcCurve: 7
+ case .closePath: 0
+ }
+ }
+
+ /// Adjusts a Command argument by a specified amount.
+ ///
+ /// A `Point` consumes two positions. So, in the example `.quadraticBezierCurve(cp: .zero, point: .zero)`:
+ /// * position 0 = Control Point X
+ /// * position 1 = Control Point Y
+ /// * position 2 = Point X
+ /// * position 3 = Point Y
+ ///
+ /// - parameter position: The index of the argument parameter to adjust.
+ /// - parameter value: The value to add to the existing value. If the current value equal `.isNaN`, than the
+ /// supplied value is used as-is.
+ /// - throws: `Path.Command.Error`
+ func adjustingArgument(at position: Int, by value: Double) throws -> Path.Command {
+ switch self {
+ case .moveTo(let point):
+ switch position {
+ case 0:
+ return .moveTo(point: point.adjusting(x: value))
+ case 1:
+ return .moveTo(point: point.adjusting(y: value))
+ default:
+ throw Path.Command.Error.invalidArgumentPosition(position, self)
+ }
+ case .lineTo(let point):
+ switch position {
+ case 0:
+ return .lineTo(point: point.adjusting(x: value))
+ case 1:
+ return .lineTo(point: point.adjusting(y: value))
+ default:
+ throw Path.Command.Error.invalidArgumentPosition(position, self)
+ }
+ case .cubicBezierCurve(let cp1, let cp2, let point):
+ switch position {
+ case 0:
+ return .cubicBezierCurve(cp1: cp1.adjusting(x: value), cp2: cp2, point: point)
+ case 1:
+ return .cubicBezierCurve(cp1: cp1.adjusting(y: value), cp2: cp2, point: point)
+ case 2:
+ return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(x: value), point: point)
+ case 3:
+ return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(y: value), point: point)
+ case 4:
+ return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(x: value))
+ case 5:
+ return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(y: value))
+ default:
+ throw Path.Command.Error.invalidArgumentPosition(position, self)
+ }
+ case .quadraticBezierCurve(let cp, let point):
+ switch position {
+ case 0:
+ return .quadraticBezierCurve(cp: cp.adjusting(x: value), point: point)
+ case 1:
+ return .quadraticBezierCurve(cp: cp.adjusting(y: value), point: point)
+ case 2:
+ return .quadraticBezierCurve(cp: cp, point: point.adjusting(x: value))
+ case 3:
+ return .quadraticBezierCurve(cp: cp, point: point.adjusting(y: value))
+ default:
+ throw Path.Command.Error.invalidArgumentPosition(position, self)
+ }
+ case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
+ switch position {
+ case 0:
+ return .ellipticalArcCurve(rx: value, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point)
+ case 1:
+ return .ellipticalArcCurve(rx: rx, ry: value, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point)
+ case 2:
+ return .ellipticalArcCurve(rx: rx, ry: ry, angle: value, largeArc: largeArc, clockwise: clockwise, point: point)
+ case 3:
+ return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: !value.isZero, clockwise: clockwise, point: point)
+ case 4:
+ return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: !value.isZero, point: point)
+ case 5:
+ return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(x: value))
+ case 6:
+ return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(y: value))
+ default:
+ throw Path.Command.Error.invalidArgumentPosition(position, self)
+ }
+ case .closePath:
+ throw Path.Command.Error.invalidAdjustment(self)
+ }
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Path.Component.swift b/third-party/SwiftSVG/Sources/Path.Component.swift
new file mode 100644
index 0000000000..6c02251142
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Path.Component.swift
@@ -0,0 +1,98 @@
+import Foundation
+
+public extension Path {
+ /// A unit of a SVG path data string.
+ enum Component {
+ case prefix(Command.Prefix)
+ case value(Double)
+
+ /// Interprets a `Path` `data` attribute into individual `Component`s for command processing.
+ public static func components(from data: String) throws -> [Component] {
+ var blocks: [String] = []
+ var block: String = ""
+
+ for scalar in data.unicodeScalars {
+ if scalar == "e" {
+ // Account for exponential value notation.
+ block.append(String(scalar))
+ continue
+ }
+
+ if CharacterSet.letters.contains(scalar) {
+ if !block.isEmpty {
+ blocks.append(block)
+ block = ""
+ }
+
+ blocks.append(String(scalar))
+ continue
+ }
+
+ if CharacterSet.whitespaces.contains(scalar) {
+ if !block.isEmpty {
+ blocks.append(block)
+ block = ""
+ }
+
+ continue
+ }
+
+ if CharacterSet(charactersIn: ",").contains(scalar) {
+ if !block.isEmpty {
+ blocks.append(block)
+ block = ""
+ }
+
+ continue
+ }
+
+ if CharacterSet(charactersIn: "-").contains(scalar) {
+ if !block.isEmpty, block.last != "e" {
+ // Again, account for exponential values.
+ blocks.append(block)
+ block = ""
+ }
+
+ block.append(String(scalar))
+ continue
+ }
+
+ if CharacterSet(charactersIn: ".").contains(scalar) {
+ if block.contains(".") {
+ // Already decimal value, this is a new value
+ blocks.append(block)
+ block = ""
+ }
+
+ block.append(String(scalar))
+ continue
+ }
+
+ if CharacterSet.decimalDigits.contains(scalar) {
+ block.append(String(scalar))
+ continue
+ }
+
+ print("Unhandled Character: \(scalar)")
+ }
+
+ if !block.isEmpty {
+ blocks.append(block)
+ block = ""
+ }
+
+ return blocks
+ .filter { !$0.isEmpty }
+ .compactMap {
+ if let prefix = Path.Command.Prefix(rawValue: $0.first!) {
+ .prefix(prefix)
+ } else if let value = Double($0) {
+ .value(value)
+ } else {
+ // throw in the future?
+ nil
+ }
+ }
+ }
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Path.ComponentParser.swift b/third-party/SwiftSVG/Sources/Path.ComponentParser.swift
new file mode 100644
index 0000000000..c64c682a6b
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Path.ComponentParser.swift
@@ -0,0 +1,275 @@
+import Swift2D
+
+public extension Path {
+ /// Utility used to construct a collection of `Path.Command` from a collection of `Path.Component`.
+ class ComponentParser {
+ /// The command currently being built
+ private var command: Path.Command?
+ /// Coordinate system being used
+ private var coordinates: Path.Command.Coordinates = .absolute
+ /// The argument position of the _command_ to be processed.
+ private var position: Int = 0
+ /// Indicates that only a single value will be processed on the next component pass.
+ private var singleValue: Bool = false
+ /// The originating coordinates of the path.
+ private var pathOrigin: Point = .nan
+ /// The last point as processed by the parser.
+ private var currentPoint: Point = .zero
+
+ public init() {}
+
+ public func parse(_ components: [Path.Component]) throws -> [Path.Command] {
+ var commands: [Path.Command] = []
+
+ try components.forEach { component in
+ if let command = try parse(component, lastCommand: commands.last) {
+ commands.append(command)
+ }
+ }
+
+ return commands
+ }
+
+ private func parse(_ component: Path.Component, lastCommand: Path.Command?) throws -> Path.Command? {
+ switch component {
+ case .prefix(let prefix):
+ setup(prefix: prefix, lastCommand: lastCommand)
+ case .value(let value):
+ try process(value: value, lastCommand: lastCommand)
+ }
+ }
+
+ private func setup(prefix: Path.Command.Prefix, lastCommand: Path.Command?) -> Path.Command? {
+ position = 0
+ singleValue = false
+
+ switch prefix {
+ case .move:
+ command = .moveTo(point: .nan)
+ coordinates = .absolute
+ case .relativeMove:
+ command = .moveTo(point: currentPoint)
+ coordinates = .relative
+ case .line:
+ command = .lineTo(point: .nan)
+ coordinates = .absolute
+ case .relativeLine:
+ command = .lineTo(point: currentPoint)
+ coordinates = .relative
+ case .horizontalLine:
+ command = .lineTo(point: currentPoint.with(x: .nan))
+ coordinates = .absolute
+ case .relativeHorizontalLine:
+ command = .lineTo(point: currentPoint)
+ coordinates = .relative
+ singleValue = true
+ case .verticalLine:
+ command = .lineTo(point: currentPoint.with(y: .nan))
+ coordinates = .absolute
+ position = 1
+ case .relativeVerticalLine:
+ command = .lineTo(point: currentPoint)
+ coordinates = .relative
+ position = 1
+ singleValue = true
+ case .cubicBezierCurve:
+ command = .cubicBezierCurve(cp1: .nan, cp2: .nan, point: .nan)
+ coordinates = .absolute
+ case .relativeCubicBezierCurve:
+ command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint)
+ coordinates = .relative
+ case .smoothCubicBezierCurve:
+ if case .cubicBezierCurve(_, let cp, _) = lastCommand {
+ command = .cubicBezierCurve(cp1: cp.reflecting(around: currentPoint), cp2: .nan, point: .nan)
+ } else {
+ command = .cubicBezierCurve(cp1: currentPoint, cp2: .nan, point: .nan)
+ }
+ coordinates = .absolute
+ position = 2
+ case .relativeSmoothCubicBezierCurve:
+ if case .cubicBezierCurve(_, let cp, _) = lastCommand {
+ command = .cubicBezierCurve(cp1: cp.reflecting(around: cp.reflecting(around: currentPoint)), cp2: currentPoint, point: currentPoint)
+ } else {
+ command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint)
+ }
+ coordinates = .relative
+ position = 2
+ case .quadraticBezierCurve:
+ command = .quadraticBezierCurve(cp: .nan, point: .nan)
+ coordinates = .absolute
+ case .relativeQuadraticBezierCurve:
+ command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint)
+ coordinates = .relative
+ case .smoothQuadraticBezierCurve:
+ if case .quadraticBezierCurve(let cp, _) = lastCommand {
+ command = .quadraticBezierCurve(cp: cp.reflecting(around: currentPoint), point: .nan)
+ } else {
+ command = .quadraticBezierCurve(cp: currentPoint, point: .nan)
+ }
+ coordinates = .absolute
+ position = 2
+ case .relativeSmoothQuadraticBezierCurve:
+ if case .quadraticBezierCurve(let cp, _) = lastCommand {
+ command = .quadraticBezierCurve(cp: cp.reflecting(around: currentPoint), point: currentPoint)
+ } else {
+ command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint)
+ }
+ coordinates = .relative
+ position = 2
+ case .ellipticalArcCurve:
+ command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan)
+ coordinates = .absolute
+ case .relativeEllipticalArcCurve:
+ command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: currentPoint)
+ coordinates = .relative
+ case .close, .relativeClose:
+ currentPoint = pathOrigin
+ reset()
+ return .closePath
+ }
+
+ return nil
+ }
+
+ private func process(value: Double, lastCommand: Path.Command?) throws -> Path.Command? {
+ if let command {
+ try continueCommand(command, with: value)
+ } else {
+ try nextCommand(with: value, lastCommand: lastCommand)
+ }
+
+ if let command, command.isComplete {
+ switch coordinates {
+ case .relative:
+ guard position == -1 else {
+ return nil
+ }
+
+ fallthrough
+ case .absolute:
+ currentPoint = command.point
+ if case .moveTo = command {
+ pathOrigin = command.point
+ }
+ reset()
+ return command
+ }
+ } else {
+ return nil
+ }
+ }
+
+ private func continueCommand(_ command: Path.Command, with value: Double) throws {
+ switch command {
+ case .moveTo, .cubicBezierCurve, .quadraticBezierCurve, .ellipticalArcCurve:
+ self.command = try command.adjustingArgument(at: position, by: value)
+ switch coordinates {
+ case .absolute:
+ position += 1
+ case .relative:
+ switch position {
+ case 0 ... (command.arguments - 2):
+ position += 1
+ case command.arguments - 1:
+ position = -1
+ default:
+ break // throw?
+ }
+ }
+ case .lineTo:
+ self.command = try command.adjustingArgument(at: position, by: value)
+ switch coordinates {
+ case .absolute:
+ position += 1
+ case .relative:
+ switch position {
+ case 0:
+ if singleValue {
+ singleValue = false
+ position = -1
+ } else {
+ position += 1
+ }
+ case 1:
+ if singleValue {
+ singleValue = false
+ }
+ position = -1
+ default:
+ break // throw?
+ }
+ }
+ case .closePath:
+ break
+ }
+ }
+
+ private func nextCommand(with value: Double, lastCommand: Path.Command?) throws {
+ guard let command = lastCommand else {
+ throw Path.Command.Error.invalidRelativeCommand
+ }
+
+ switch command {
+ case .moveTo:
+ switch coordinates {
+ case .absolute:
+ self.command = .lineTo(point: Point(x: value, y: .nan))
+ position = 1
+ case .relative:
+ let c = Path.Command.lineTo(point: command.point)
+ self.command = try c.adjustingArgument(at: 0, by: value)
+ position = 1
+ }
+ case .lineTo:
+ switch coordinates {
+ case .absolute:
+ self.command = .lineTo(point: Point(x: value, y: .nan))
+ position = 1
+ case .relative:
+ let c = Path.Command.lineTo(point: command.point)
+ self.command = try c.adjustingArgument(at: 0, by: value)
+ position = 1
+ }
+ case .cubicBezierCurve:
+ switch coordinates {
+ case .absolute:
+ self.command = .cubicBezierCurve(cp1: Point(x: value, y: .nan), cp2: .nan, point: .nan)
+ position = 1
+ case .relative:
+ let c = Path.Command.cubicBezierCurve(cp1: command.point, cp2: command.point, point: command.point)
+ self.command = try c.adjustingArgument(at: 0, by: value)
+ position = 1
+ }
+ case .quadraticBezierCurve:
+ switch coordinates {
+ case .absolute:
+ self.command = .quadraticBezierCurve(cp: Point(x: value, y: .nan), point: .nan)
+ position = 1
+ case .relative:
+ let c = Path.Command.quadraticBezierCurve(cp: command.point, point: command.point)
+ self.command = try c.adjustingArgument(at: 0, by: value)
+ position = 1
+ }
+ case .ellipticalArcCurve:
+ switch coordinates {
+ case .absolute:
+ self.command = .ellipticalArcCurve(rx: value, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan)
+ position = 1
+ case .relative:
+ let c = Path.Command.ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: command.point)
+ self.command = try c.adjustingArgument(at: 0, by: value)
+ position = 1
+ }
+ case .closePath:
+ break
+ }
+ }
+
+ private func reset() {
+ command = nil
+ coordinates = .absolute
+ position = 0
+ singleValue = false
+ }
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Path.swift b/third-party/SwiftSVG/Sources/Path.swift
new file mode 100644
index 0000000000..9c02c65704
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Path.swift
@@ -0,0 +1,101 @@
+import XMLCoder
+
+/// Generic element to define a shape.
+///
+/// A path is defined by including a ‘path’ element in a SVG document which contains a **d="(path data)"**
+/// attribute, where the **‘d’** attribute contains the moveto, line, curve (both Cubic and Quadratic Bézier),
+/// arc and closepath instructions.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)
+/// | [W3](https://www.w3.org/TR/SVG11/paths.html)
+public struct Path: Element {
+
+ /// The definition of the outline of a shape.
+ public var data: String = ""
+
+ // MARK: CoreAttributes
+
+ public var id: String?
+
+ // MARK: PresentationAttributes
+
+ public var fillColor: String?
+ public var fillOpacity: Double?
+ public var fillRule: Fill.Rule?
+ public var strokeColor: String?
+ public var strokeWidth: Double?
+ public var strokeOpacity: Double?
+ public var strokeLineCap: Stroke.LineCap?
+ public var strokeLineJoin: Stroke.LineJoin?
+ public var strokeMiterLimit: Double?
+ public var transform: String?
+
+ // MARK: StylingAttributes
+
+ public var style: String?
+
+ enum CodingKeys: String, CodingKey {
+ case data = "d"
+ case id
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+ case style
+ }
+
+ public init() {}
+
+ public init(data: String) {
+ self.init()
+ self.data = data
+ }
+
+ public init(commands: [Path.Command]) {
+ self.init()
+ data = commands.map(\.description).joined()
+ }
+}
+
+extension Path: CommandRepresentable {
+ public func commands() throws -> [Command] {
+ try PathProcessor(data: data).commands()
+ }
+}
+
+extension Path: CustomStringConvertible {
+ public var description: String {
+ ""
+ }
+}
+
+extension Path: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ .attribute
+ }
+}
+
+extension Path: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ .attribute
+ }
+}
+
+extension Path: Equatable {
+ public static func == (lhs: Path, rhs: Path) -> Bool {
+ do {
+ let lhsCommands = try lhs.commands()
+ let rhsCommands = try rhs.commands()
+ return lhsCommands == rhsCommands
+ } catch {
+ return false
+ }
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Polygon.swift b/third-party/SwiftSVG/Sources/Polygon.swift
new file mode 100644
index 0000000000..84320b929f
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Polygon.swift
@@ -0,0 +1,82 @@
+import XMLCoder
+
+/// Defines a closed shape consisting of a set of connected straight line segments.
+///
+/// The last point is connected to the first point. For open shapes, see the `Polyline` element. If an odd number of
+/// coordinates is provided, then the element is in error.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon)
+/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolygonElement)
+public struct Polygon: Element {
+
+ /// The points that make up the polygon.
+ public var points: String = ""
+
+ // MARK: CoreAttributes
+
+ public var id: String?
+
+ // MARK: PresentationAttributes
+
+ public var fillColor: String?
+ public var fillOpacity: Double?
+ public var fillRule: Fill.Rule?
+ public var strokeColor: String?
+ public var strokeWidth: Double?
+ public var strokeOpacity: Double?
+ public var strokeLineCap: Stroke.LineCap?
+ public var strokeLineJoin: Stroke.LineJoin?
+ public var strokeMiterLimit: Double?
+ public var transform: String?
+
+ // MARK: StylingAttributes
+
+ public var style: String?
+
+ enum CodingKeys: String, CodingKey {
+ case points
+ case id
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+ case style
+ }
+
+ public init() {}
+
+ public init(points: String) {
+ self.points = points
+ }
+}
+
+extension Polygon: CommandRepresentable {
+ public func commands() throws -> [Path.Command] {
+ try PolygonProcessor(points: points).commands()
+ }
+}
+
+extension Polygon: CustomStringConvertible {
+ public var description: String {
+ ""
+ }
+}
+
+extension Polygon: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ .attribute
+ }
+}
+
+extension Polygon: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ .attribute
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Polyline.swift b/third-party/SwiftSVG/Sources/Polyline.swift
new file mode 100644
index 0000000000..5fb8c930c4
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Polyline.swift
@@ -0,0 +1,81 @@
+import XMLCoder
+
+/// SVG basic shape that creates straight lines connecting several points.
+///
+/// Typically a polyline is used to create open shapes as the last point doesn't have to be connected to the first
+/// point. For closed shapes see the `Polygon` element.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline)
+/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolylineElement)
+public struct Polyline: Element {
+
+ public var points: String = ""
+
+ // MARK: CoreAttributes
+
+ public var id: String?
+
+ // MARK: PresentationAttributes
+
+ public var fillColor: String?
+ public var fillOpacity: Double?
+ public var fillRule: Fill.Rule?
+ public var strokeColor: String?
+ public var strokeWidth: Double?
+ public var strokeOpacity: Double?
+ public var strokeLineCap: Stroke.LineCap?
+ public var strokeLineJoin: Stroke.LineJoin?
+ public var strokeMiterLimit: Double?
+ public var transform: String?
+
+ // MARK: StylingAttributes
+
+ public var style: String?
+
+ enum CodingKeys: String, CodingKey {
+ case points
+ case id
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+ case style
+ }
+
+ public init() {}
+
+ public init(points: String) {
+ self.points = points
+ }
+}
+
+extension Polyline: CommandRepresentable {
+ public func commands() throws -> [Path.Command] {
+ try PolylineProcessor(points: points).commands()
+ }
+}
+
+extension Polyline: CustomStringConvertible {
+ public var description: String {
+ ""
+ }
+}
+
+extension Polyline: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ .attribute
+ }
+}
+
+extension Polyline: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ .attribute
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/PresentationAttributes.swift b/third-party/SwiftSVG/Sources/PresentationAttributes.swift
new file mode 100644
index 0000000000..5cd6598df3
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/PresentationAttributes.swift
@@ -0,0 +1,120 @@
+import Foundation
+import Swift2D
+
+public protocol PresentationAttributes {
+ var fillColor: String? { get set }
+ var fillOpacity: Double? { get set }
+ var fillRule: Fill.Rule? { get set }
+ var strokeColor: String? { get set }
+ var strokeWidth: Double? { get set }
+ var strokeOpacity: Double? { get set }
+ var strokeLineCap: Stroke.LineCap? { get set }
+ var strokeLineJoin: Stroke.LineJoin? { get set }
+ var strokeMiterLimit: Double? { get set }
+ var transform: String? { get set }
+}
+
+enum PresentationAttributesKeys: String, CodingKey {
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+}
+
+public extension PresentationAttributes {
+ var presentationDescription: String {
+ var attributes: [String] = []
+
+ if let fillColor {
+ attributes.append("\(PresentationAttributesKeys.fillColor.rawValue)=\"\(fillColor)\"")
+ }
+ if let fillOpacity {
+ attributes.append("\(PresentationAttributesKeys.fillOpacity.rawValue)=\"\(fillOpacity)\"")
+ }
+ if let fillRule {
+ attributes.append("\(PresentationAttributesKeys.fillRule.rawValue)=\"\(fillRule.description)\"")
+ }
+ if let strokeColor {
+ attributes.append("\(PresentationAttributesKeys.strokeColor.rawValue)=\"\(strokeColor)\"")
+ }
+ if let strokeWidth {
+ attributes.append("\(PresentationAttributesKeys.strokeWidth.rawValue)=\"\(strokeWidth)\"")
+ }
+ if let strokeOpacity {
+ attributes.append("\(PresentationAttributesKeys.strokeOpacity.rawValue)=\"\(strokeOpacity)\"")
+ }
+ if let strokeLineCap {
+ attributes.append("\(PresentationAttributesKeys.strokeLineCap.rawValue)=\"\(strokeLineCap.description)\"")
+ }
+ if let strokeLineJoin {
+ attributes.append("\(PresentationAttributesKeys.strokeLineJoin.rawValue)=\"\(strokeLineJoin.description)\"")
+ }
+ if let strokeMiterLimit {
+ attributes.append("\(PresentationAttributesKeys.strokeMiterLimit.rawValue)=\"\(strokeMiterLimit)\"")
+ }
+ if let transform {
+ attributes.append("\(PresentationAttributesKeys.transform.rawValue)=\"\(transform)\"")
+ }
+
+ return attributes.joined(separator: " ")
+ }
+
+ var transformations: [Transformation] {
+ let value = transform?.replacingOccurrences(of: " ", with: "") ?? ""
+ guard !value.isEmpty else {
+ return []
+ }
+
+ let values = value.split(separator: ")").map { $0.appending(")") }
+ return values.compactMap { Transformation($0) }
+ }
+
+ var fill: Fill? {
+ get {
+ if fillColor == nil, fillOpacity == nil {
+ return nil
+ }
+
+ var fill = Fill()
+ fill.color = fillColor ?? "black"
+ fill.opacity = fillOpacity ?? 1.0
+ return fill
+ }
+ set {
+ fillColor = newValue?.color
+ fillOpacity = newValue?.opacity
+ fillRule = newValue?.rule
+ }
+ }
+
+ var stroke: Stroke? {
+ get {
+ if strokeColor == nil, strokeOpacity == nil {
+ return nil
+ }
+
+ var stroke = Stroke()
+ stroke.color = strokeColor ?? "black"
+ stroke.opacity = strokeOpacity ?? 1.0
+ stroke.width = strokeWidth ?? 1.0
+ stroke.lineCap = strokeLineCap ?? .butt
+ stroke.lineJoin = strokeLineJoin ?? .miter
+ stroke.miterLimit = strokeMiterLimit
+ return stroke
+ }
+ set {
+ strokeColor = newValue?.color
+ strokeOpacity = newValue?.opacity
+ strokeWidth = newValue?.width
+ strokeLineCap = newValue?.lineCap
+ strokeLineJoin = newValue?.lineJoin
+ strokeMiterLimit = newValue?.miterLimit
+ }
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Rectangle.swift b/third-party/SwiftSVG/Sources/Rectangle.swift
new file mode 100644
index 0000000000..4f663b846d
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Rectangle.swift
@@ -0,0 +1,115 @@
+import Swift2D
+import XMLCoder
+
+/// Basic SVG shape that draws rectangles, defined by their position, width, and height.
+///
+/// The values used for the x- and y-axis rounded corner radii are determined implicitly
+/// if the ‘rx’ or ‘ry’ attributes (or both) are not specified, or are specified but with invalid values.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect)
+/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#RectElement)
+public struct Rectangle: Element {
+
+ /// The x-axis coordinate of the side of the rectangle which
+ /// has the smaller x-axis coordinate value.
+ public var x: Double = 0.0
+ /// The y-axis coordinate of the side of the rectangle which
+ /// has the smaller y-axis coordinate value
+ public var y: Double = 0.0
+ /// The width of the rectangle.
+ public var width: Double = 0.0
+ /// The height of the rectangle.
+ public var height: Double = 0.0
+ /// For rounded rectangles, the x-axis radius of the ellipse used
+ /// to round off the corners of the rectangle.
+ public var rx: Double?
+ /// For rounded rectangles, the y-axis radius of the ellipse used
+ /// to round off the corners of the rectangle.
+ public var ry: Double?
+
+ // MARK: CoreAttributes
+
+ public var id: String?
+
+ // MARK: PresentationAttributes
+
+ public var fillColor: String?
+ public var fillOpacity: Double?
+ public var fillRule: Fill.Rule?
+ public var strokeColor: String?
+ public var strokeWidth: Double?
+ public var strokeOpacity: Double?
+ public var strokeLineCap: Stroke.LineCap?
+ public var strokeLineJoin: Stroke.LineJoin?
+ public var strokeMiterLimit: Double?
+ public var transform: String?
+
+ // MARK: StylingAttributes
+
+ public var style: String?
+
+ enum CodingKeys: String, CodingKey {
+ case x
+ case y
+ case width
+ case height
+ case rx
+ case ry
+ case id
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+ case style
+ }
+
+ public init() {}
+
+ public init(x: Double, y: Double, width: Double, height: Double, rx: Double? = nil, ry: Double? = nil) {
+ self.x = x
+ self.y = y
+ self.width = width
+ self.height = height
+ self.rx = rx
+ self.ry = ry
+ }
+}
+
+extension Rectangle: CustomStringConvertible {
+ public var description: String {
+ var desc = ""
+ }
+}
+
+extension Rectangle: DirectionalCommandRepresentable {
+ public func commands(clockwise: Bool) throws -> [Path.Command] {
+ RectangleProcessor(rectangle: self).commands(clockwise: clockwise)
+ }
+}
+
+extension Rectangle: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ .attribute
+ }
+}
+
+extension Rectangle: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ .attribute
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/SVG+Swift2D.swift b/third-party/SwiftSVG/Sources/SVG+Swift2D.swift
new file mode 100644
index 0000000000..abf62de434
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/SVG+Swift2D.swift
@@ -0,0 +1,62 @@
+import Foundation
+import Swift2D
+
+public extension SVG {
+ /// Original size of the document image.
+ ///
+ /// Primarily uses the `viewBox` attribute, and will fallback to the 'pixelSize'
+ var originalSize: Size {
+ (viewBoxSize ?? pixelSize) ?? .zero
+ }
+
+ /// Size of the design in a square 'viewBox'.
+ ///
+ /// All paths created by this framework are outputted in a 'square'.
+ var outputSize: Size {
+ let size = (pixelSize ?? viewBoxSize) ?? .zero
+ let maxDimension = max(size.width, size.height)
+ return Size(width: maxDimension, height: maxDimension)
+ }
+
+ /// Size derived from the `viewBox` document attribute
+ var viewBoxSize: Size? {
+ guard let viewBox else {
+ return nil
+ }
+
+ let components = viewBox.components(separatedBy: .whitespaces)
+ guard components.count == 4 else {
+ return nil
+ }
+
+ guard let width = Double(components[2]) else {
+ return nil
+ }
+
+ guard let height = Double(components[3]) else {
+ return nil
+ }
+
+ return Size(width: width, height: height)
+ }
+
+ /// Size derived from the 'width' & 'height' document attributes
+ var pixelSize: Size? {
+ guard let width, !width.isEmpty else {
+ return nil
+ }
+
+ guard let height, !height.isEmpty else {
+ return nil
+ }
+
+ let widthRawValue = width.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil)
+ let heightRawValue = height.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil)
+
+ guard let w = Double(widthRawValue), let h = Double(heightRawValue) else {
+ return nil
+ }
+
+ return Size(width: w, height: h)
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/SVG.swift b/third-party/SwiftSVG/Sources/SVG.swift
new file mode 100644
index 0000000000..cbf3117bbc
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/SVG.swift
@@ -0,0 +1,160 @@
+import Foundation
+import XMLCoder
+
+/// SVG is a language for describing two-dimensional graphics in XML.
+///
+/// The svg element is a container that defines a new coordinate system and viewport. It is used as the outermost
+/// element of SVG documents, but it can also be used to embed a SVG fragment inside an SVG or HTML document.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg)
+/// | [W3](https://www.w3.org/TR/SVG11/)
+public struct SVG: Container {
+
+ public var viewBox: String?
+ public var width: String?
+ public var height: String?
+ public var title: String?
+ public var desc: String?
+
+ // Container
+ public var circles: [Circle]?
+ public var ellipses: [Ellipse]?
+ public var groups: [Group]?
+ public var lines: [Line]?
+ public var paths: [Path]?
+ public var polygons: [Polygon]?
+ public var polylines: [Polyline]?
+ public var rectangles: [Rectangle]?
+ public var texts: [Text]?
+
+ /// A non-optional, non-spaced representation of the `title`.
+ public var name: String {
+ let name = title ?? "SVG Document"
+ let newTitle = name.components(separatedBy: .punctuationCharacters).joined(separator: "_")
+ return newTitle.replacingOccurrences(of: " ", with: "_")
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case width
+ case height
+ case viewBox
+ case title
+ case desc
+ case circles = "circle"
+ case ellipses = "ellipse"
+ case groups = "g"
+ case lines = "line"
+ case paths = "path"
+ case polylines = "polyline"
+ case polygons = "polygon"
+ case rectangles = "rect"
+ case texts = "text"
+ }
+
+ public init() {}
+
+ public init(width: Int, height: Int) {
+ self.width = "\(width)px"
+ self.height = "\(height)px"
+ viewBox = "0 0 \(width) \(height)"
+ }
+
+ public static func make(from url: URL) throws -> SVG {
+ guard FileManager.default.fileExists(atPath: url.path) else {
+ throw CocoaError(.fileNoSuchFile)
+ }
+
+ let data = try Data(contentsOf: url)
+ return try make(with: data)
+ }
+
+ public static func make(with data: Data, decoder: XMLDecoder = XMLDecoder()) throws -> SVG {
+ try decoder.decode(SVG.self, from: data)
+ }
+
+ /// A collection of all `Path`s in the document.
+ public func subpaths() throws -> [Path] {
+ var output: [Path] = []
+ let _transformations: [Transformation] = []
+
+ if let circles {
+ try output.append(contentsOf: circles.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let ellipses {
+ try output.append(contentsOf: ellipses.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let rectangles {
+ try output.append(contentsOf: rectangles.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let polygons {
+ try output.append(contentsOf: polygons.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let polylines {
+ try output.append(contentsOf: polylines.compactMap { try $0.path(applying: _transformations) })
+ }
+
+ if let paths {
+ try output.append(contentsOf: paths.map { try $0.path(applying: _transformations) })
+ }
+
+ if let groups {
+ try groups.forEach {
+ try output.append(contentsOf: $0.subpaths(applying: _transformations))
+ }
+ }
+
+ return output
+ }
+
+ /// A singular path that represents all of the `Command`s within the document.
+ public func coalescedPath() throws -> Path {
+ let paths = try subpaths()
+ let commands = try paths.flatMap { try $0.commands() }
+ return Path(commands: commands)
+ }
+}
+
+extension SVG: CustomStringConvertible {
+ public var description: String {
+ var contents: String = ""
+
+ if let title {
+ contents.append("\n\(title)")
+ }
+
+ if let desc {
+ contents.append("\n\(desc)")
+ }
+
+ contents.append(containerDescription)
+
+ return ""
+ }
+}
+
+extension SVG: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ switch key {
+ case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox:
+ .attribute
+ default:
+ .element
+ }
+ }
+}
+
+extension SVG: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ switch key {
+ case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox:
+ .attribute
+ default:
+ .element
+ }
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Stroke.swift b/third-party/SwiftSVG/Sources/Stroke.swift
new file mode 100644
index 0000000000..d68cba3dd4
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Stroke.swift
@@ -0,0 +1,64 @@
+import Swift2D
+
+public struct Stroke {
+
+ public var color: String?
+ public var width: Double?
+ public var opacity: Double?
+ public var lineCap: LineCap = .butt
+ public var lineJoin: LineJoin = .miter
+ public var miterLimit: Double?
+
+ public init() {}
+
+ /// Presentation attribute defining the shape to be used at the end of open subpaths when they are stroked.
+ ///
+ /// The default `LineCap` is `.butt`
+ public enum LineCap: String, Sendable, Codable, CaseIterable {
+ /// The stroke for each subpath does not extend beyond its two endpoints.
+ case butt
+ /// The end of each subpath the stroke will be extended by a half circle with a diameter equal to the stroke
+ /// width.
+ case round
+ /// The end of each subpath the stroke will be extended by a rectangle with a width equal to half the width of
+ /// the stroke and a height equal to the width of the stroke.
+ case square
+ }
+
+ /// Presentation attribute defining the shape to be used at the corners of paths when they are stroked.
+ ///
+ /// The default `LineJoin` is `.miter`
+ public enum LineJoin: String, Sendable, Codable, CaseIterable {
+ /// An arcs corner is to be used to join path segments.
+ ///
+ /// The arcs shape is formed by extending the outer edges of the stroke at the join point with arcs that have
+ /// the same curvature as the outer edges at the join point.
+ case arcs
+ /// The bevel value indicates that a bevelled corner is to be used to join path segments.
+ case bevel
+ /// Indicates that a sharp corner is to be used to join path segments.
+ ///
+ /// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until
+ /// they intersect.
+ case miter
+ /// A sharp corner is to be used to join path segments.
+ ///
+ /// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until
+ /// they intersect.
+ case miterClip = "miter-clip"
+ /// The round value indicates that a round corner is to be used to join path segments.
+ case round
+ }
+}
+
+extension Stroke.LineCap: CustomStringConvertible {
+ public var description: String {
+ rawValue
+ }
+}
+
+extension Stroke.LineJoin: CustomStringConvertible {
+ public var description: String {
+ rawValue
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/StylingAttributes.swift b/third-party/SwiftSVG/Sources/StylingAttributes.swift
new file mode 100644
index 0000000000..8c597a42f4
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/StylingAttributes.swift
@@ -0,0 +1,17 @@
+public protocol StylingAttributes {
+ var style: String? { get set }
+}
+
+enum StylingAttributesKeys: String, CodingKey {
+ case style
+}
+
+public extension StylingAttributes {
+ var stylingDescription: String {
+ if let style {
+ "\(StylingAttributesKeys.style.rawValue)=\"\(style)\""
+ } else {
+ ""
+ }
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Text.swift b/third-party/SwiftSVG/Sources/Text.swift
new file mode 100644
index 0000000000..d54f62fcf6
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Text.swift
@@ -0,0 +1,110 @@
+import Foundation
+import XMLCoder
+
+/// Graphics element consisting of text
+///
+/// It's possible to apply a gradient, pattern, clipping path, mask, or filter to `Text`, like any other SVG graphics element.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text)
+/// | [W3](https://www.w3.org/TR/SVG11/text.html#TextElement)
+public struct Text: Element {
+
+ public var value: String = ""
+ public var x: Double?
+ public var y: Double?
+ public var dx: Double?
+ public var dy: Double?
+
+ // MARK: CoreAttributes
+
+ public var id: String?
+
+ // MARK: PresentationAttributes
+
+ public var fillColor: String?
+ public var fillOpacity: Double?
+ public var fillRule: Fill.Rule?
+ public var strokeColor: String?
+ public var strokeWidth: Double?
+ public var strokeOpacity: Double?
+ public var strokeLineCap: Stroke.LineCap?
+ public var strokeLineJoin: Stroke.LineJoin?
+ public var strokeMiterLimit: Double?
+ public var transform: String?
+
+ // MARK: StylingAttributes
+
+ public var style: String?
+
+ enum CodingKeys: String, CodingKey {
+ case value = ""
+ case x
+ case y
+ case dx
+ case dy
+ case id
+ case fillColor = "fill"
+ case fillOpacity = "fill-opacity"
+ case fillRule = "fill-rule"
+ case strokeColor = "stroke"
+ case strokeWidth = "stroke-width"
+ case strokeOpacity = "stroke-opacity"
+ case strokeLineCap = "stroke-linecap"
+ case strokeLineJoin = "stroke-linejoin"
+ case strokeMiterLimit = "stroke-miterlimit"
+ case transform
+ case style
+ }
+
+ public init() {}
+
+ public init(value: String) {
+ self.value = value
+ }
+}
+
+extension Text: CustomStringConvertible {
+ public var description: String {
+ var components: [String] = []
+
+ if let x, !x.isNaN, !x.isZero {
+ components.append(String(format: "x=\"%.5f\"", x))
+ }
+ if let y, !y.isNaN, !y.isZero {
+ components.append(String(format: "y=\"%.5f\"", y))
+ }
+ if let dx, !dx.isNaN, !dx.isZero {
+ components.append(String(format: "dx=\"%.5f\"", dx))
+ }
+ if let dy, !dy.isNaN, !dy.isZero {
+ components.append(String(format: "dy=\"%.5f\"", dy))
+ }
+
+ components.append(attributeDescription)
+
+ return "\(value)"
+ }
+}
+
+extension Text: DynamicNodeDecoding {
+ public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
+ switch key {
+ case CodingKeys.value:
+ .element
+ default:
+ .attribute
+ }
+ }
+}
+
+extension Text: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ switch key {
+ case CodingKeys.value:
+ .element
+ default:
+ .attribute
+ }
+ }
+}
diff --git a/third-party/SwiftSVG/Sources/Transformation.swift b/third-party/SwiftSVG/Sources/Transformation.swift
new file mode 100644
index 0000000000..3fb086d55e
--- /dev/null
+++ b/third-party/SwiftSVG/Sources/Transformation.swift
@@ -0,0 +1,113 @@
+import Foundation
+import Swift2D
+
+/// A modification that should be applied to an element and its children.
+///
+/// If a list of transforms is provided, then the net effect is as if each transform had been specified separately in
+/// the order provided.
+///
+/// For example,
+/// ```
+///
+///
+///
+/// ```
+/// is functionally equivalent to:
+/// ```
+///
+///
+///
+///
+///
+///
+///
+///
+///
+/// ```
+///
+/// The ‘transform’ attribute is applied to an element before processing any other coordinate or length values supplied
+/// for that element.
+///
+/// ## Documentation
+/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform)
+/// | [W3](https://www.w3.org/TR/SVG11/coords.html#TransformAttribute)
+public enum Transformation {
+ /// Moves an object by x & y. (Y is assumed to be '0' if not provided)
+ case translate(x: Double, y: Double)
+ /// Specifies a transformation in the form of a transformation matrix of six values.
+ case matrix(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double)
+
+ public enum Prefix: String, CaseIterable {
+ case translate
+ case matrix
+ }
+
+ /// Initializes a new `Transformation` with a raw SVG transformation string.
+ public init?(_ string: String) {
+ guard let prefix = Prefix.allCases.first(where: { string.lowercased().hasPrefix($0.rawValue) }) else {
+ return nil
+ }
+
+ switch prefix {
+ case .translate:
+ guard let start = string.firstIndex(of: "(") else {
+ return nil
+ }
+
+ guard let stop = string.lastIndex(of: ")") else {
+ return nil
+ }
+
+ var substring = String(string[start ... stop])
+ substring = substring.replacingOccurrences(of: "(", with: "")
+ substring = substring.replacingOccurrences(of: ")", with: "")
+
+ var components = substring.split(separator: " ", omittingEmptySubsequences: true).map { String($0) }
+ components = components.flatMap { $0.components(separatedBy: ",") }
+
+ let values = components.compactMap { Double($0) }.map { Double($0) }
+ guard values.count > 0 else {
+ return nil
+ }
+
+ if values.count > 1 {
+ self = .translate(x: values[0], y: values[1])
+ } else {
+ self = .translate(x: values[0], y: 0.0)
+ }
+ case .matrix:
+ guard let start = string.firstIndex(of: "(") else {
+ return nil
+ }
+
+ guard let stop = string.lastIndex(of: ")") else {
+ return nil
+ }
+
+ var substring = String(string[start ... stop])
+ substring = substring.replacingOccurrences(of: "(", with: "")
+ substring = substring.replacingOccurrences(of: ")", with: "")
+
+ var components = substring.split(separator: " ", omittingEmptySubsequences: true).map { String($0) }
+ components = components.flatMap { $0.components(separatedBy: ",") }
+
+ let values = components.compactMap { Double($0) }.map { Double($0) }
+ guard values.count > 5 else {
+ return nil
+ }
+
+ self = .matrix(a: values[0], b: values[1], c: values[2], d: values[3], e: values[4], f: values[5])
+ }
+ }
+}
+
+extension Transformation: CustomStringConvertible {
+ public var description: String {
+ switch self {
+ case .translate(let x, let y):
+ "translate(\(x), \(y))"
+ case .matrix(let a, let b, let c, let d, let e, let f):
+ "matrix(\(a), \(b), \(c), \(d), \(e), \(f))"
+ }
+ }
+}
diff --git a/third-party/VectorPlus/BUILD b/third-party/VectorPlus/BUILD
new file mode 100644
index 0000000000..c7f57a85a1
--- /dev/null
+++ b/third-party/VectorPlus/BUILD
@@ -0,0 +1,19 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "VectorPlus",
+ module_name = "VectorPlus",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//third-party/SwiftColor",
+ "//third-party/SwiftSVG",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/third-party/VectorPlus/Sources/CoreGraphics/CGContext+Render.swift b/third-party/VectorPlus/Sources/CoreGraphics/CGContext+Render.swift
new file mode 100644
index 0000000000..a6f3e94d73
--- /dev/null
+++ b/third-party/VectorPlus/Sources/CoreGraphics/CGContext+Render.swift
@@ -0,0 +1,72 @@
+import Swift2D
+import SwiftColor
+import SwiftSVG
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+public extension CGContext {
+ func render(path: Path, from: Rect, to: Rect) throws {
+ saveGState()
+
+ let cgPath = CGMutablePath()
+
+ let commands = (try? path.commands()) ?? []
+ for (idx, command) in commands.enumerated() {
+ let previous: Point? = if idx > 0 {
+ commands[idx - 1].previousPoint
+ } else {
+ nil
+ }
+
+ cgPath.addCommand(command, from: from, to: to, previousPoint: previous)
+ }
+
+ if let fill = path.fill {
+ let _color = Pigment(fill.color ?? "black")
+ if _color.alpha != 0.0 {
+ let cgColor = CGColor.make(_color)
+ let color = cgColor.copy(alpha: CGFloat(fill.opacity ?? 1.0)) ?? cgColor
+ let rule = fill.rule.cgFillRule
+
+ setFillColor(color)
+ addPath(cgPath)
+ fillPath(using: rule)
+ }
+ }
+
+ if let stroke = path.stroke {
+ let _color = Pigment(stroke.color ?? "black")
+ if _color.alpha != 0.0 {
+ let cgColor = CGColor.make(_color)
+ let color = cgColor.copy(alpha: CGFloat(stroke.opacity ?? 1.0)) ?? cgColor
+ let width = stroke.width ?? 1.0
+ let lineWidth = width * (to.size.width / from.size.width)
+
+ setLineWidth(CGFloat(lineWidth))
+ setStrokeColor(color)
+ setLineCap(stroke.lineCap.cgLineCap)
+ setLineJoin(stroke.lineJoin.cgLineJoin)
+ if let miterLimit = stroke.miterLimit, stroke.lineJoin == .miter {
+ setMiterLimit(CGFloat(miterLimit))
+ }
+ addPath(cgPath)
+ strokePath()
+ }
+ }
+
+ if path.fill == nil, path.stroke == nil {
+ let color = CGColor(srgbRed: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
+ setFillColor(color)
+ addPath(cgPath)
+ fillPath(using: path.fill?.rule.cgFillRule ?? .winding)
+ }
+
+ restoreGState()
+ }
+
+ func render(path: Path, in rect: Rect) throws {
+ try render(path: path, from: rect, to: rect)
+ }
+}
+
+#endif
diff --git a/third-party/VectorPlus/Sources/CoreGraphics/CGMutablePath+Instruction.swift b/third-party/VectorPlus/Sources/CoreGraphics/CGMutablePath+Instruction.swift
new file mode 100644
index 0000000000..f810e6d883
--- /dev/null
+++ b/third-party/VectorPlus/Sources/CoreGraphics/CGMutablePath+Instruction.swift
@@ -0,0 +1,45 @@
+import Swift2D
+import SwiftSVG
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+public extension CGMutablePath {
+ /// Adds a `Path.Command` to a _CoreGraphics path_, using the provided rectangles to correctly scale the parameters.
+ ///
+ /// - parameter command: The `Path.Command` to append
+ /// - parameter from: The `Rect` which originally had the instruction. This is typically the `Document.originalSize`.
+ /// - parameter to: The `Rect` defining the new size.
+ /// - parameter previousPoint: The last `Point`, used for Elliptical Arc calculations
+ func addCommand(_ command: Path.Command, from: Rect, to: Rect, previousPoint: Point? = nil) {
+ let translated = command.translate(from: from, to: to)
+ switch translated {
+ case .moveTo(let point):
+ move(to: CGPoint(point))
+ case .lineTo(let point):
+ addLine(to: CGPoint(point))
+ case .cubicBezierCurve(let cp1, let cp2, let point):
+ addCurve(to: CGPoint(point), control1: CGPoint(cp1), control2: CGPoint(cp2))
+ case .quadraticBezierCurve(let cp, let point):
+ addQuadCurve(to: CGPoint(point), control: CGPoint(cp))
+ case .ellipticalArcCurve(_, _, _, _, _, let point):
+ guard let previousPoint else {
+ addLine(to: CGPoint(point))
+ return
+ }
+
+ do {
+ let curves = try command.convertToCubicBezierCurves(with: previousPoint)
+ for curve in curves {
+ addCommand(curve, from: from, to: to)
+ }
+ } catch {
+ print(error)
+ addLine(to: CGPoint(point))
+ }
+ case .closePath:
+ closeSubpath()
+ }
+ }
+}
+
+#endif
diff --git a/third-party/VectorPlus/Sources/CoreGraphics/Fill+CoreGraphics.swift b/third-party/VectorPlus/Sources/CoreGraphics/Fill+CoreGraphics.swift
new file mode 100644
index 0000000000..e038784c2c
--- /dev/null
+++ b/third-party/VectorPlus/Sources/CoreGraphics/Fill+CoreGraphics.swift
@@ -0,0 +1,14 @@
+import Foundation
+import SwiftSVG
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+public extension Fill.Rule {
+ var cgFillRule: CGPathFillRule {
+ switch self {
+ case .evenOdd: .evenOdd
+ case .nonZero: .winding
+ }
+ }
+}
+#endif
diff --git a/third-party/VectorPlus/Sources/CoreGraphics/SVG+CGPath.swift b/third-party/VectorPlus/Sources/CoreGraphics/SVG+CGPath.swift
new file mode 100644
index 0000000000..ef4cadbb66
--- /dev/null
+++ b/third-party/VectorPlus/Sources/CoreGraphics/SVG+CGPath.swift
@@ -0,0 +1,37 @@
+import Swift2D
+import SwiftSVG
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+public extension SVG {
+ func path(size: Size) -> CGPath {
+ guard size.height > 0.0, size.width > 0.0 else {
+ return CGMutablePath()
+ }
+
+ guard let paths = try? subpaths() else {
+ return CGMutablePath()
+ }
+
+ let from = Rect(origin: .zero, size: originalSize)
+ let to = Rect(origin: .zero, size: size)
+
+ let path = CGMutablePath()
+
+ for p in paths {
+ let commands = (try? p.commands()) ?? []
+ for (idx, command) in commands.enumerated() {
+ let previous: Point? = if idx > 0 {
+ commands[idx - 1].previousPoint
+ } else {
+ nil
+ }
+
+ path.addCommand(command, from: from, to: to, previousPoint: previous)
+ }
+ }
+
+ return path
+ }
+}
+#endif
diff --git a/third-party/VectorPlus/Sources/CoreGraphics/Stroke+CoreGraphics.swift b/third-party/VectorPlus/Sources/CoreGraphics/Stroke+CoreGraphics.swift
new file mode 100644
index 0000000000..3744223181
--- /dev/null
+++ b/third-party/VectorPlus/Sources/CoreGraphics/Stroke+CoreGraphics.swift
@@ -0,0 +1,25 @@
+import Foundation
+import SwiftSVG
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+public extension Stroke.LineCap {
+ var cgLineCap: CGLineCap {
+ switch self {
+ case .butt: .butt
+ case .round: .round
+ case .square: .square
+ }
+ }
+}
+
+public extension Stroke.LineJoin {
+ var cgLineJoin: CGLineJoin {
+ switch self {
+ case .bevel: .bevel
+ case .arcs, .miter, .miterClip: .miter
+ case .round: .round
+ }
+ }
+}
+#endif
diff --git a/third-party/VectorPlus/Sources/Fill+VectorPlus.swift b/third-party/VectorPlus/Sources/Fill+VectorPlus.swift
new file mode 100644
index 0000000000..c7a66cf9fb
--- /dev/null
+++ b/third-party/VectorPlus/Sources/Fill+VectorPlus.swift
@@ -0,0 +1,29 @@
+import SwiftColor
+import SwiftSVG
+
+public extension Fill {
+ @available(*, deprecated, renamed: "pigment")
+ var swiftColor: Pigment? { pigment }
+
+ var pigment: Pigment? {
+ guard let color, !color.isEmpty else {
+ return nil
+ }
+
+ let _color = Pigment(color)
+ guard _color.alpha != 0.0 else {
+ return nil
+ }
+
+ return _color
+ }
+}
+
+public extension Fill.Rule {
+ var coreGraphicsDescription: String {
+ switch self {
+ case .evenOdd: ".evenOdd"
+ case .nonZero: ".winding"
+ }
+ }
+}
diff --git a/third-party/VectorPlus/Sources/Path+VectorPlus.swift b/third-party/VectorPlus/Sources/Path+VectorPlus.swift
new file mode 100644
index 0000000000..6511f6bbac
--- /dev/null
+++ b/third-party/VectorPlus/Sources/Path+VectorPlus.swift
@@ -0,0 +1,34 @@
+import Swift2D
+import SwiftSVG
+
+public extension Path {
+ func asCoreGraphicsDescription(variable: String = "path", originalSize: Size) throws -> String {
+ var outputs: [String] = []
+ let commands = (try? commands()) ?? []
+ for (idx, command) in commands.enumerated() {
+ let previous: Point? = if idx > 0 {
+ commands[idx - 1].previousPoint
+ } else {
+ nil
+ }
+
+ let method = command.coreGraphicsDescription(originalSize: originalSize, previousPoint: previous)
+ let code = "\(variable)\(method)"
+ outputs.append(code)
+ }
+ return outputs.joined(separator: "\n ")
+ }
+}
+
+public extension Path.Command {
+ var previousPoint: Point {
+ switch self {
+ case .moveTo(let point): point
+ case .lineTo(let point): point
+ case .cubicBezierCurve(_, _, let point): point
+ case .quadraticBezierCurve(_, let point): point
+ case .ellipticalArcCurve(_, _, _, _, _, let point): point
+ case .closePath: .zero
+ }
+ }
+}
diff --git a/third-party/VectorPlus/Sources/Path.Command+VectorPlus.swift b/third-party/VectorPlus/Sources/Path.Command+VectorPlus.swift
new file mode 100644
index 0000000000..226e57a643
--- /dev/null
+++ b/third-party/VectorPlus/Sources/Path.Command+VectorPlus.swift
@@ -0,0 +1,216 @@
+import Foundation
+import Swift2D
+import SwiftSVG
+
+public extension Path.Command {
+ /// Uses the _Power of Math_ to translate a commands controls/points from one `Rect` to another `Rect`.
+ func translate(from: Rect, to: Rect) -> Path.Command {
+ switch self {
+ case .moveTo(let point):
+ let _point = VectorPoint(point: point, in: from).translate(to: to)
+ return .moveTo(point: _point)
+ case .lineTo(let point):
+ let _point = VectorPoint(point: point, in: from).translate(to: to)
+ return .lineTo(point: _point)
+ case .cubicBezierCurve(let cp1, let cp2, let point):
+ let _cp1 = VectorPoint(point: cp1, in: from).translate(to: to)
+ let _cp2 = VectorPoint(point: cp2, in: from).translate(to: to)
+ let _point = VectorPoint(point: point, in: from).translate(to: to)
+ return .cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point)
+ case .quadraticBezierCurve(let cp, let point):
+ let _cp = VectorPoint(point: cp, in: from).translate(to: to)
+ let _point = VectorPoint(point: point, in: from).translate(to: to)
+ return .quadraticBezierCurve(cp: _cp, point: _point)
+ case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
+ let _rx = rx * (from.size.maxRadius / to.size.minRadius)
+ let _ry = ry * (from.size.maxRadius / to.size.minRadius)
+ let _point = VectorPoint(point: point, in: from).translate(to: to)
+ return .ellipticalArcCurve(rx: _rx, ry: _ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: _point)
+ case .closePath:
+ return self
+ }
+ }
+}
+
+public extension Path.Command {
+ func coreGraphicsDescription(originalSize: Size, previousPoint: Point? = nil) -> String {
+ let rect = Rect(origin: .zero, size: originalSize)
+
+ switch self {
+ case .moveTo(let point):
+ let _point = VectorPoint(point: point, in: rect)
+ return ".move(to: \(_point.coreGraphicsDescription))"
+ case .lineTo(let point):
+ let _point = VectorPoint(point: point, in: rect)
+ return ".addLine(to: \(_point.coreGraphicsDescription))"
+ case .cubicBezierCurve(let cp1, let cp2, let point):
+ let _cp1 = VectorPoint(point: cp1, in: rect)
+ let _cp2 = VectorPoint(point: cp2, in: rect)
+ let _point = VectorPoint(point: point, in: rect)
+ return ".addCurve(to: \(_point.coreGraphicsDescription), control1: \(_cp1.coreGraphicsDescription), control2: \(_cp2.coreGraphicsDescription))"
+ case .quadraticBezierCurve(let cp, let point):
+ let _cp = VectorPoint(point: cp, in: rect)
+ let _point = VectorPoint(point: point, in: rect)
+ return ".addQuadCurve(to: \(_point.coreGraphicsDescription), control: \(_cp.coreGraphicsDescription))"
+ case .ellipticalArcCurve(_, _, _, _, _, let point):
+ guard let previousPoint else {
+ return Path.Command.lineTo(point: point).coreGraphicsDescription(originalSize: originalSize)
+ }
+
+ do {
+ let curves = try convertToCubicBezierCurves(with: previousPoint)
+ return curves.map { $0.coreGraphicsDescription(originalSize: originalSize) }.joined(separator: "\n")
+ } catch {
+ print(error)
+ return Path.Command.lineTo(point: point).coreGraphicsDescription(originalSize: originalSize)
+ }
+ case .closePath:
+ return ".closeSubpath()"
+ }
+ }
+}
+
+extension Path.Command {
+ /// Converts an `.ellipticalArcCurve` into one or more `.cubicBezierCurve`s.
+ /// https://github.com/colinmeinke/svg-arc-to-cubic-bezier/blob/master/src/index.js
+ func convertToCubicBezierCurves(with previousPoint: Point) throws -> [Path.Command] {
+ guard case let .ellipticalArcCurve(rx, ry, angle, largeArg, clockwise, point) = self else {
+ throw Path.Command.Error.message("\(#function); Only .ellipticalArcCurve is allowed.")
+ }
+
+ var curves: [Path.Command] = []
+
+ guard rx > 0.0, ry > 0.0 else {
+ throw Path.Command.Error.message("\(#function); rx/ry must be greater than 0.0 (zero).")
+ }
+
+ let sinφ = sin(angle * (.pi * 2.0) / 360.0)
+ let cosφ = cos(angle * (.pi * 2.0) / 360.0)
+
+ let pxp = cosφ * (previousPoint.x - point.x) / 2 + sinφ * (previousPoint.y - point.y) / 2.0
+ let pyp = -sinφ * (previousPoint.x - point.x) / 2 + cosφ * (previousPoint.y - point.y) / 2.0
+
+ guard pxp != 0.0, pyp != 0.0 else {
+ throw Path.Command.Error.message("\(#function); math")
+ }
+
+ var _rx = abs(rx)
+ var _ry = abs(ry)
+
+ let λ = pow(pxp, 2.0) / pow(_rx, 2.0) + pow(pyp, 2.0) / pow(_ry, 2.0)
+
+ if λ > 1.0 {
+ _rx *= sqrt(λ)
+ _ry *= sqrt(λ)
+ }
+
+ let _arcCenter = arcCenter(previousPoint: previousPoint, point: point, rx: _rx, ry: _ry, largeArc: largeArg, clockwise: clockwise, sinφ: sinφ, cosφ: cosφ, pxp: pxp, pyp: pyp)
+ let center = _arcCenter.center
+ var angle1 = _arcCenter.angle1
+ var angle2 = _arcCenter.angle2
+
+ var ratio = abs(angle2) / ((.pi * 2.0) / 4.0)
+ if abs(1.0 - ratio) < 0.0000001 {
+ ratio = 1.0
+ }
+
+ let segments = max(ceil(ratio), 1)
+
+ angle2 /= segments
+
+ var rawCurves: [(Point, Point, Point)] = []
+ for _ in 0 ... Int(segments) {
+ rawCurves.append(approximateUnitArc(angle1: angle1, angle2: angle2))
+ angle1 += angle2
+ }
+
+ for rawCurf in rawCurves {
+ let _cp1 = mapToEllipse(point: rawCurf.0, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center)
+ let _cp2 = mapToEllipse(point: rawCurf.1, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center)
+ let _point = mapToEllipse(point: rawCurf.2, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center)
+ curves.append(.cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point))
+ }
+
+ return curves
+ }
+}
+
+private func arcCenter(previousPoint: Point, point: Point, rx: Double, ry: Double, largeArc: Bool, clockwise: Bool, sinφ: Double, cosφ: Double, pxp: Double, pyp: Double) ->
+ (center: Point, angle1: Double, angle2: Double)
+{
+
+ let rxsq = pow(rx, 2.0)
+ let rysq = pow(ry, 2.0)
+ let pxpsq = pow(pxp, 2.0)
+ let pypsq = pow(pyp, 2.0)
+
+ var radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq)
+ if radicant < 0.0 {
+ radicant = 0.0
+ }
+
+ radicant /= (rxsq * pypsq) + (rysq * pxpsq)
+ radicant = sqrt(radicant) * (largeArc == clockwise ? -1.0 : 1.0)
+
+ let centerxp = radicant * rx / ry * pyp
+ let centeryp = radicant * -ry / rx * pxp
+
+ let centerx = cosφ * centerxp - sinφ * centeryp + (previousPoint.x + point.x) / 2.0
+ let centery = sinφ * centerxp + cosφ * centeryp + (previousPoint.x + point.x) / 2.0
+
+ let vx1 = (pxp - centerxp) / rx
+ let vy1 = (pyp - centeryp) / ry
+ let vx2 = (-pxp - centerxp) / rx
+ let vy2 = (-pyp - centeryp) / ry
+
+ let angle1 = vectorAngle(u: Point(x: 1, y: 0), v: Point(x: vx1, y: vy1))
+ var angle2 = vectorAngle(u: Point(x: vx1, y: vy1), v: Point(x: vx2, y: vy2))
+
+ if clockwise == false, angle2 > 0.0 {
+ angle2 -= (.pi * 2.0)
+ } else if clockwise == true, angle2 < 0.0 {
+ angle2 += (.pi * 2.0)
+ }
+
+ return (Point(x: centerx, y: centery), angle1, angle2)
+}
+
+private func vectorAngle(u: Point, v: Point) -> Double {
+ let sign: Double = ((u.x * v.y - u.y * v.x) < 0.0) ? -1.0 : 1.0
+ var dot = u.x * v.x + u.y * v.y
+ if dot > 1.0 {
+ dot = 1.0
+ } else if dot < -1.0 {
+ dot = -1.0
+ }
+
+ return sign * acos(dot)
+}
+
+private func approximateUnitArc(angle1: Double, angle2: Double) -> (Point, Point, Point) {
+ // If 90 degree circular arc, use a constant
+ // as derived from http://spencermortensen.com/articles/bezier-circle
+ let a: Double = switch angle2 {
+ case 1.5707963267948966:
+ 0.551915024494
+ case -1.5707963267948966:
+ -0.551915024494
+ default:
+ 4.0 / 3.0 * tan(angle2 / 4.0)
+ }
+
+ let x1 = cos(angle1)
+ let y1 = sin(angle1)
+ let x2 = cos(angle1 + angle2)
+ let y2 = sin(angle1 + angle2)
+
+ return (Point(x: x1 - y1 * a, y: y1 + x1 * a), Point(x: x2 + y2 * a, y: y2 - x2 * 1), Point(x: x2, y: y2))
+}
+
+private func mapToEllipse(point: Point, rx: Double, ry: Double, sinφ: Double, cosφ: Double, center: Point) -> Point {
+ let x = point.x * rx
+ let y = point.y * ry
+ let xp = cosφ * x - sinφ * y
+ let yp = sinφ * x + cosφ * y
+ return Point(x: xp + center.x, y: yp + center.y)
+}
diff --git a/third-party/VectorPlus/Sources/Pigment+VectorPlus.swift b/third-party/VectorPlus/Sources/Pigment+VectorPlus.swift
new file mode 100644
index 0000000000..ea9b3e7e04
--- /dev/null
+++ b/third-party/VectorPlus/Sources/Pigment+VectorPlus.swift
@@ -0,0 +1,7 @@
+import SwiftColor
+
+public extension Pigment {
+ var coreGraphicsDescription: String {
+ "CGColor(srgbRed: \(red), green: \(green), blue: \(blue), alpha: \(alpha))"
+ }
+}
diff --git a/third-party/VectorPlus/Sources/Point+VectorPlus.swift b/third-party/VectorPlus/Sources/Point+VectorPlus.swift
new file mode 100644
index 0000000000..716a66f023
--- /dev/null
+++ b/third-party/VectorPlus/Sources/Point+VectorPlus.swift
@@ -0,0 +1,7 @@
+import Swift2D
+
+public extension Point {
+ var coreGraphicsDescription: String {
+ "CGPoint(x: \(x), y: \(y))"
+ }
+}
diff --git a/third-party/VectorPlus/Sources/Rect+VectorPlus.swift b/third-party/VectorPlus/Sources/Rect+VectorPlus.swift
new file mode 100644
index 0000000000..d66f66f6d6
--- /dev/null
+++ b/third-party/VectorPlus/Sources/Rect+VectorPlus.swift
@@ -0,0 +1,7 @@
+import Swift2D
+
+public extension Rect {
+ var coreGraphicsDescription: String {
+ "CGRect(origin: \(origin.coreGraphicsDescription), size: \(size.coreGraphicsDescription))"
+ }
+}
diff --git a/third-party/VectorPlus/Sources/SVG+AppleSymbols.swift b/third-party/VectorPlus/Sources/SVG+AppleSymbols.swift
new file mode 100644
index 0000000000..8eb5759507
--- /dev/null
+++ b/third-party/VectorPlus/Sources/SVG+AppleSymbols.swift
@@ -0,0 +1,324 @@
+import Foundation
+import Swift2D
+import SwiftSVG
+import XMLCoder
+
+public extension SVG {
+
+ static func encodeDocument(_ document: SVG, encoder: XMLEncoder = XMLEncoder()) throws -> Data {
+ let rootAttributes: [String: String] = [
+ "version": "1.1",
+ "xmlns": "http://www.w3.org/2000/svg",
+ "xmlns:xlink": "http://www.w3.org/1999/xlink",
+ ]
+ let header = XMLHeader(version: 1.0, encoding: "UTF-8", standalone: nil)
+ return try encoder.encode(document, withRootKey: "svg", rootAttributes: rootAttributes, header: header)
+ }
+
+ static func appleSymbols(path: Path, in rect: Rect) throws -> SVG {
+ var document = SVG(width: 3300, height: 2200)
+ document.groups = try [.appleSymbolsNotes, .appleSymbolsGuides, .appleSymbols(path: path, in: rect)]
+ return document
+ }
+}
+
+public extension Group {
+ static var appleSymbolsNotes: Group {
+ var group = Group()
+
+ group.id = "Notes"
+ group.rectangles = []
+ group.lines = []
+ group.texts = []
+ group.groups = []
+
+ var artboard = Rectangle(x: 0, y: 0, width: 3300, height: 2200)
+ artboard.id = "artboard"
+ artboard.style = "fill:white;opacity:1"
+ group.rectangles?.append(artboard)
+
+ var topLine = Line(x1: 263, y1: 292, x2: 3036, y2: 292)
+ topLine.style = "fill:none;stroke:black;opacity:1;stroke-width:0.5;"
+ group.lines?.append(topLine)
+
+ var bottomLine = Line(x1: 263, y1: 1903, x2: 3036, y2: 1903)
+ bottomLine.style = "fill:none;stroke:black;opacity:1;stroke-width:0.5;"
+ group.lines?.append(bottomLine)
+
+ var text = Text()
+ text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
+ text.transform = "matrix(1 0 0 1 263 322)"
+ text.value = "Weight/Scale Variations"
+ group.texts?.append(text)
+
+ text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;text-anchor:middle"
+ text.transform = "matrix(1 0 0 1 559.711 322)"
+ text.value = "Ultralight"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 856.422 322)"
+ text.value = "Thin"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 1153.13 322)"
+ text.value = "Light"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 1449.84 322)"
+ text.value = "Regular"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 1746.56 322)"
+ text.value = "Medium"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 2043.27 322)"
+ text.value = "Semibold"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 2339.98 322)"
+ text.value = "Bold"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 2636.69 322)"
+ text.value = "Heavy"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 2933.4 322)"
+ text.value = "Black"
+ group.texts?.append(text)
+
+ var path = Path(data: "M 9.24805 0.830078 C 13.5547 0.830078 17.1387 -2.74414 17.1387 -7.05078 C 17.1387 -11.3574 13.5449 -14.9316 9.23828 -14.9316 C 4.94141 -14.9316 1.36719 -11.3574 1.36719 -7.05078 C 1.36719 -2.74414 4.95117 0.830078 9.24805 0.830078 Z M 9.24805 -0.654297 C 5.70312 -0.654297 2.87109 -3.49609 2.87109 -7.05078 C 2.87109 -10.6055 5.69336 -13.4473 9.23828 -13.4473 C 12.793 -13.4473 15.6348 -10.6055 15.6445 -7.05078 C 15.6543 -3.49609 12.8027 -0.654297 9.24805 -0.654297 Z M 9.22852 -3.42773 C 9.69727 -3.42773 9.9707 -3.74023 9.9707 -4.25781 L 9.9707 -6.31836 L 12.1973 -6.31836 C 12.6953 -6.31836 13.0371 -6.57227 13.0371 -7.04102 C 13.0371 -7.51953 12.7148 -7.7832 12.1973 -7.7832 L 9.9707 -7.7832 L 9.9707 -10.0098 C 9.9707 -10.5273 9.69727 -10.8496 9.22852 -10.8496 C 8.75977 -10.8496 8.50586 -10.5078 8.50586 -10.0098 L 8.50586 -7.7832 L 6.29883 -7.7832 C 5.78125 -7.7832 5.44922 -7.51953 5.44922 -7.04102 C 5.44922 -6.57227 5.80078 -6.31836 6.29883 -6.31836 L 8.50586 -6.31836 L 8.50586 -4.25781 C 8.50586 -3.75977 8.75977 -3.42773 9.22852 -3.42773 Z")
+ var subGroup = Group("", path: path, transform: "matrix(1 0 0 1 263 1933)")
+ group.groups?.append(subGroup)
+
+ path.data = "M 11.709 2.91016 C 17.1582 2.91016 21.6699 -1.60156 21.6699 -7.05078 C 21.6699 -12.4902 17.1484 -17.0117 11.6992 -17.0117 C 6.25977 -17.0117 1.74805 -12.4902 1.74805 -7.05078 C 1.74805 -1.60156 6.26953 2.91016 11.709 2.91016 Z M 11.709 1.25 C 7.09961 1.25 3.41797 -2.44141 3.41797 -7.05078 C 3.41797 -11.6504 7.08984 -15.3516 11.6992 -15.3516 C 16.3086 -15.3516 20 -11.6504 20.0098 -7.05078 C 20.0195 -2.44141 16.3184 1.25 11.709 1.25 Z M 11.6895 -2.41211 C 12.207 -2.41211 12.5195 -2.77344 12.5195 -3.33984 L 12.5195 -6.23047 L 15.5762 -6.23047 C 16.123 -6.23047 16.5039 -6.51367 16.5039 -7.03125 C 16.5039 -7.55859 16.1426 -7.86133 15.5762 -7.86133 L 12.5195 -7.86133 L 12.5195 -10.9277 C 12.5195 -11.5039 12.207 -11.8555 11.6895 -11.8555 C 11.1719 -11.8555 10.8789 -11.4844 10.8789 -10.9277 L 10.8789 -7.86133 L 7.83203 -7.86133 C 7.26562 -7.86133 6.89453 -7.55859 6.89453 -7.03125 C 6.89453 -6.51367 7.28516 -6.23047 7.83203 -6.23047 L 10.8789 -6.23047 L 10.8789 -3.33984 C 10.8789 -2.79297 11.1719 -2.41211 11.6895 -2.41211 Z"
+ subGroup.paths = [path]
+ subGroup.transform = "matrix(1 0 0 1 281.506 1933)"
+ group.groups?.append(subGroup)
+
+ path.data = "M 14.9707 5.67383 C 21.9336 5.67383 27.6953 -0.078125 27.6953 -7.04102 C 27.6953 -14.0039 21.9238 -19.7559 14.9609 -19.7559 C 8.00781 -19.7559 2.25586 -14.0039 2.25586 -7.04102 C 2.25586 -0.078125 8.01758 5.67383 14.9707 5.67383 Z M 14.9707 3.85742 C 8.93555 3.85742 4.08203 -1.00586 4.08203 -7.04102 C 4.08203 -13.0762 8.92578 -17.9395 14.9609 -17.9395 C 21.0059 -17.9395 25.8594 -13.0762 25.8691 -7.04102 C 25.8789 -1.00586 21.0156 3.85742 14.9707 3.85742 Z M 14.9512 -1.06445 C 15.5176 -1.06445 15.8691 -1.45508 15.8691 -2.06055 L 15.8691 -6.13281 L 20.1074 -6.13281 C 20.6934 -6.13281 21.1133 -6.46484 21.1133 -7.02148 C 21.1133 -7.59766 20.7227 -7.93945 20.1074 -7.93945 L 15.8691 -7.93945 L 15.8691 -12.1875 C 15.8691 -12.8027 15.5176 -13.1934 14.9512 -13.1934 C 14.3848 -13.1934 14.0625 -12.7832 14.0625 -12.1875 L 14.0625 -7.93945 L 9.83398 -7.93945 C 9.21875 -7.93945 8.80859 -7.59766 8.80859 -7.02148 C 8.80859 -6.46484 9.23828 -6.13281 9.83398 -6.13281 L 14.0625 -6.13281 L 14.0625 -2.06055 C 14.0625 -1.47461 14.3848 -1.06445 14.9512 -1.06445 Z"
+ subGroup.paths = [path]
+ subGroup.transform = "matrix(1 0 0 1 304.924 1933)"
+ group.groups?.append(subGroup)
+
+ text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
+ text.transform = "matrix(1 0 0 1 263 1953)"
+ text.value = "Design Variations"
+ group.texts?.append(text)
+
+ text.style = "none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
+ text.transform = "matrix(1 0 0 1 263 1971)"
+ text.value = "Symbols are supported in up to nine weights and three scales."
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 263 1989)"
+ text.value = "For optimal layout with text and other symbols, vertically align"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 263 2007)"
+ text.value = "symbols with the adjacent text."
+ group.texts?.append(text)
+
+ var rect = Rectangle(x: 776, y: 1919, width: 3, height: 14)
+ rect.style = "fill:#00AEEF;stroke:none;opacity:0.4;"
+ group.rectangles?.append(rect)
+
+ path.data = "M 10.5273 0 L 12.373 0 L 7.17773 -14.0918 L 5.43945 -14.0918 L 0.244141 0 L 2.08984 0 L 3.50586 -4.0332 L 9.11133 -4.0332 Z M 6.2793 -11.9531 L 6.33789 -11.9531 L 8.59375 -5.52734 L 4.02344 -5.52734 Z"
+ subGroup.paths = [path]
+ subGroup.transform = "matrix(1 0 0 1 779 1933)"
+ group.groups?.append(subGroup)
+
+ rect.x = 791.617
+ group.rectangles?.append(rect)
+
+ text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
+ text.transform = "matrix(1 0 0 1 776 1953)"
+ text.value = "Margins"
+ group.texts?.append(text)
+
+ text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
+ text.transform = "matrix(1 0 0 1 776 1971)"
+ text.value = "Leading and trailing margins on the left and right side of each symbol"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 776 1989)"
+ text.value = "can be adjusted by modifying the width of the blue rectangles."
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 776 2007)"
+ text.value = "Modifications are automatically applied proportionally to all"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 776 2025)"
+ text.value = "scales and weights."
+ group.texts?.append(text)
+
+ path.data = "M 2.83203 3.11523 L 4.375 4.6582 C 5.22461 5.48828 6.19141 5.41992 7.06055 4.46289 L 17.2754 -6.66016 C 17.7051 -6.36719 18.0957 -6.37695 18.5645 -6.47461 L 19.6094 -6.68945 L 20.3027 -5.99609 L 20.2539 -5.47852 C 20.1855 -4.95117 20.3516 -4.53125 20.8496 -4.0332 L 21.6602 -3.22266 C 22.168 -2.71484 22.8223 -2.68555 23.3008 -3.16406 L 26.5527 -6.41602 C 27.0312 -6.89453 27.0117 -7.54883 26.5039 -8.05664 L 25.6836 -8.87695 C 25.1855 -9.375 24.7754 -9.55078 24.2383 -9.47266 L 23.7109 -9.41406 L 23.0566 -10.0781 L 23.3398 -11.2207 C 23.4863 -11.7871 23.3398 -12.2559 22.7148 -12.8613 L 20.3027 -15.2539 C 16.7578 -18.7793 12.2266 -18.6719 9.11133 -15.5371 C 8.69141 -15.1074 8.64258 -14.5215 8.91602 -14.0918 C 9.15039 -13.7207 9.62891 -13.4961 10.2734 -13.6621 C 11.7871 -14.043 13.3008 -13.9258 14.7852 -12.9199 L 14.1602 -11.3379 C 13.9258 -10.752 13.9453 -10.2734 14.1797 -9.83398 L 3.01758 0.439453 C 2.08008 1.30859 1.97266 2.25586 2.83203 3.11523 Z M 10.6738 -15.1465 C 13.3398 -17.1387 16.6504 -16.8262 19.0527 -14.4141 L 21.6797 -11.8066 C 21.9141 -11.5723 21.9434 -11.3867 21.8848 -11.0938 L 21.5039 -9.53125 L 23.0762 -7.95898 L 24.043 -8.04688 C 24.3262 -8.07617 24.4141 -8.05664 24.6387 -7.83203 L 25.2637 -7.20703 L 22.5098 -4.46289 L 21.8848 -5.07812 C 21.6602 -5.30273 21.6406 -5.40039 21.6699 -5.68359 L 21.7578 -6.64062 L 20.1953 -8.20312 L 18.5742 -7.89062 C 18.291 -7.83203 18.1445 -7.83203 17.9102 -8.07617 L 15.7324 -10.2539 C 15.5078 -10.4883 15.4785 -10.625 15.6055 -10.9473 L 16.5527 -13.2227 C 14.9512 -14.7559 12.8418 -15.6055 10.8008 -14.9512 C 10.7129 -14.9219 10.6445 -14.9414 10.6152 -14.9805 C 10.5859 -15.0293 10.5859 -15.0781 10.6738 -15.1465 Z M 4.10156 2.41211 C 3.61328 1.91406 3.78906 1.61133 4.12109 1.30859 L 15.0781 -8.80859 L 16.3086 -7.57812 L 6.15234 3.34961 C 5.84961 3.68164 5.46875 3.7793 5.06836 3.37891 Z"
+ subGroup.paths = [path]
+ subGroup.transform = "matrix(1 0 0 1 1289 1933)"
+ group.groups?.append(subGroup)
+
+ text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
+ text.transform = "matrix(1 0 0 1 1289 1953)"
+ text.value = "Exporting"
+ group.texts?.append(text)
+
+ text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
+ text.transform = "matrix(1 0 0 1 1289 1971)"
+ text.value = "Symbols should be outlined when exporting to ensure the"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 1289 1989)"
+ text.value = "design is preserved when submitting to Xcode."
+ group.texts?.append(text)
+
+ text.id = "template-version"
+ text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;text-anchor:end;"
+ text.transform = "matrix(1 0 0 1 3036 1933)"
+ text.value = "Template v.2.0"
+ group.texts?.append(text)
+
+ text.id = nil
+ text.transform = "matrix(1 0 0 1 3036 1969)"
+ text.value = "Typeset at 100 points"
+ group.texts?.append(text)
+
+ text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
+ text.transform = "matrix(1 0 0 1 263 726)"
+ text.value = "Small"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 263 1156)"
+ text.value = "Medium"
+ group.texts?.append(text)
+
+ text.transform = "matrix(1 0 0 1 263 1586)"
+ text.value = "Large"
+ group.texts?.append(text)
+
+ return group
+ }
+
+ static var appleSymbolsGuides: Group {
+ var group = Group()
+
+ group.id = "Guides"
+ group.groups = []
+ group.lines = []
+
+ var hRef = Group()
+ hRef.id = "H-reference"
+ hRef.style = "fill:#27AAE1;stroke:none;"
+ hRef.transform = "matrix(1 0 0 1 339 696)"
+ hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")]
+ group.groups?.append(hRef)
+
+ var baseline = Line(x1: 263, y1: 696, x2: 3036, y2: 696)
+ baseline.id = "Baseline-S"
+ baseline.style = "fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.577;"
+ group.lines?.append(baseline)
+
+ var capline = Line(x1: 263, y1: 625.541, x2: 3036, y2: 625.541)
+ capline.id = "Capline-S"
+ capline.style = "fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.577;"
+ group.lines?.append(capline)
+
+ hRef.transform = "matrix(1 0 0 1 339 1126)"
+ hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")]
+ group.groups?.append(hRef)
+
+ baseline.id = "Baseline-M"
+ baseline.y1 = 1126
+ baseline.y2 = 1126
+ group.lines?.append(baseline)
+
+ capline.id = "Capline-M"
+ capline.y1 = 1055.54
+ capline.y2 = 1055.54
+ group.lines?.append(capline)
+
+ hRef.transform = "matrix(1 0 0 1 339 1556)"
+ hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")]
+ group.groups?.append(hRef)
+
+ baseline.id = "Baseline-L"
+ baseline.y1 = 1556
+ baseline.y2 = 1556
+ group.lines?.append(baseline)
+
+ capline.id = "Capline-L"
+ capline.y1 = 1485.54
+ capline.y2 = 1485.54
+ group.lines?.append(capline)
+
+ var margin = Line(x1: 1399.72, y1: 1030.79, x2: 1399.72, y2: 1150.12)
+ margin.id = "left-margin"
+ margin.style = "fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
+ group.lines?.append(margin)
+
+ margin = Line(x1: 1499.97, y1: 1030.79, x2: 1499.97, y2: 1150.12)
+ margin.id = "right-margin"
+ margin.style = "fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
+ group.lines?.append(margin)
+
+ return group
+ }
+
+ static func appleSymbols(path: Path, in rect: Rect) throws -> Group {
+ var group = Group()
+
+ group.id = "Symbols"
+
+ let translations: [(name: String, size: Float, center: Point)] = [
+ ("Ultralight-S", 0.75, .init(x: 559.0, y: 661.0)),
+ ("Thin-S", 0.76, .init(x: 857.0, y: 661.0)),
+ ("Light-S", 0.78, .init(x: 1153.0, y: 661.0)),
+ ("Regular-S", 0.79, .init(x: 1449.5, y: 661.0)),
+ ("Medium-S", 0.80, .init(x: 1747.0, y: 661.0)),
+ ("Semibold-S", 0.81, .init(x: 2043.0, y: 661.0)),
+ ("Bold-S", 0.82, .init(x: 2340.0, y: 661.0)),
+ ("Heavy-S", 0.85, .init(x: 2636.5, y: 661.0)),
+ ("Black-S", 0.86, .init(x: 2933.0, y: 661.0)),
+ ("Ultralight-M", 0.95, .init(x: 559.0, y: 1091.0)),
+ ("Thin-M", 0.96, .init(x: 857.0, y: 1091.0)),
+ ("Light-M", 0.98, .init(x: 1153.0, y: 1091.0)),
+ ("Regular-M", 1.00, .init(x: 1449.5, y: 1091.0)),
+ ("Medium-M", 1.02, .init(x: 1747.0, y: 1091.0)),
+ ("Semibold-M", 1.03, .init(x: 2043.0, y: 1091.0)),
+ ("Bold-M", 1.05, .init(x: 2340.0, y: 1091.0)),
+ ("Heavy-M", 1.07, .init(x: 2636.5, y: 1091.0)),
+ ("Black-M", 1.10, .init(x: 2933.0, y: 1091.0)),
+ ("Ultralight-L", 1.22, .init(x: 559.0, y: 1521.0)),
+ ("Thin-L", 1.24, .init(x: 857.0, y: 1521.0)),
+ ("Light-L", 1.26, .init(x: 1153.0, y: 1521.0)),
+ ("Regular-L", 1.28, .init(x: 1449.5, y: 1521.0)),
+ ("Medium-L", 1.30, .init(x: 1747.0, y: 1521.0)),
+ ("Semibold-L", 1.31, .init(x: 2043.0, y: 1521.0)),
+ ("Bold-L", 1.33, .init(x: 2340.0, y: 1521.0)),
+ ("Heavy-L", 1.36, .init(x: 2636.5, y: 1521.0)),
+ ("Black-L", 1.39, .init(x: 2933.0, y: 1521.0)),
+ ]
+
+ group.groups = try translations.map { symbol -> Group in
+ let size = Size(width: 100.0 * symbol.size, height: 100.0 * symbol.size)
+ let to = Rect(origin: .zero, size: size)
+ let commands = try path.commands().map { $0.translate(from: rect, to: to) }
+ let p = Path(commands: commands)
+ let matrixOrigin = Point(x: symbol.center.x - size.width / 2.0, y: symbol.center.y - size.height / 2.0)
+ let matrix: Transformation = .matrix(a: 1, b: 0, c: 0, d: 1, e: matrixOrigin.x, f: matrixOrigin.y)
+ return Group(symbol.name, path: p, transform: matrix.description)
+ }
+
+ return group
+ }
+}
+
+private extension Group {
+ init(_ id: String, path: Path, transform: String) {
+ self.init()
+ self.id = id
+ self.transform = transform
+ paths = [path]
+ }
+}
diff --git a/third-party/VectorPlus/Sources/SVG+Template.swift b/third-party/VectorPlus/Sources/SVG+Template.swift
new file mode 100644
index 0000000000..6dbd962d84
--- /dev/null
+++ b/third-party/VectorPlus/Sources/SVG+Template.swift
@@ -0,0 +1,56 @@
+import Foundation
+import Swift2D
+import SwiftSVG
+
+public extension SVG {
+ func asImageViewSubclass() throws -> String {
+ let instructions = try asCoreGraphicsDescription()
+ let renders = try asCGContextDescription()
+
+ return imageViewSubclassTemplate
+ .replacingOccurrences(of: "{{name}}", with: name)
+ .replacingOccurrences(of: "{{width}}", with: String(format: "%.1f", originalSize.width))
+ .replacingOccurrences(of: "{{height}}", with: String(format: "%.1f", originalSize.height))
+ .replacingOccurrences(of: "{{instructions}}", with: instructions)
+ .replacingOccurrences(of: "{{render}}", with: renders)
+ }
+}
+
+private extension SVG {
+ func asCoreGraphicsDescription(variable: String = "path") throws -> String {
+ try subpaths().map { try $0.asCoreGraphicsDescription(variable: variable, originalSize: originalSize) }.joined(separator: "\n ")
+ }
+
+ func asCGContextDescription() throws -> String {
+ var outputs: [String] = []
+
+ let paths = try subpaths()
+ try paths.forEach { path in
+ let instructions = try path.asCoreGraphicsDescription(variable: "path", originalSize: originalSize)
+ let fillColor = path.fill?.pigment?.coreGraphicsDescription ?? "nil"
+ let fillOpacity = (path.fillOpacity != nil) ? "\(path.fillOpacity!)" : "nil"
+ let fillRule = (path.fillRule ?? .nonZero).coreGraphicsDescription
+ let strokeColor = path.stroke?.pigment?.coreGraphicsDescription ?? "nil"
+ let strokeOpacity = (path.strokeOpacity != nil) ? "\(path.strokeOpacity!)" : "nil"
+ let strokeWidth = (path.strokeWidth != nil) ? "\(path.strokeWidth!) * (size.width / width)" : "nil"
+ let strokeLineCap = (path.strokeLineCap != nil) ? "\(path.strokeLineCap!.coreGraphicsDescription)" : "nil"
+ let strokeLineJoin = (path.strokeLineJoin != nil) ? "\(path.strokeLineJoin!.coreGraphicsDescription)" : "nil"
+ let strokeMiterLimit = (path.strokeMiterLimit != nil) ? "\(path.strokeMiterLimit!)" : "nil"
+
+ outputs.append(contextTemplate
+ .replacingOccurrences(of: "{{instructions}}", with: instructions)
+ .replacingOccurrences(of: "{{fillColor}}", with: fillColor)
+ .replacingOccurrences(of: "{{fillOpacity}}", with: fillOpacity)
+ .replacingOccurrences(of: "{{fillRule}}", with: fillRule)
+ .replacingOccurrences(of: "{{strokeColor}}", with: strokeColor)
+ .replacingOccurrences(of: "{{strokeOpacity}}", with: strokeOpacity)
+ .replacingOccurrences(of: "{{strokeWidth}}", with: strokeWidth)
+ .replacingOccurrences(of: "{{strokeLineCap}}", with: strokeLineCap)
+ .replacingOccurrences(of: "{{strokeLineJoin}}", with: strokeLineJoin)
+ .replacingOccurrences(of: "{{strokeMiterLimit}}", with: strokeMiterLimit)
+ )
+ }
+
+ return outputs.joined(separator: "\n ")
+ }
+}
diff --git a/third-party/VectorPlus/Sources/Size+VectorPlus.swift b/third-party/VectorPlus/Sources/Size+VectorPlus.swift
new file mode 100644
index 0000000000..3ec4a7bbd4
--- /dev/null
+++ b/third-party/VectorPlus/Sources/Size+VectorPlus.swift
@@ -0,0 +1,7 @@
+import Swift2D
+
+public extension Size {
+ var coreGraphicsDescription: String {
+ "CGSize(width: \(width), height: \(height))"
+ }
+}
diff --git a/third-party/VectorPlus/Sources/Stroke+VectorPlus.swift b/third-party/VectorPlus/Sources/Stroke+VectorPlus.swift
new file mode 100644
index 0000000000..0d933d2aca
--- /dev/null
+++ b/third-party/VectorPlus/Sources/Stroke+VectorPlus.swift
@@ -0,0 +1,40 @@
+import SwiftColor
+import SwiftSVG
+
+public extension Stroke {
+ @available(*, deprecated, renamed: "pigment")
+ var swiftColor: Pigment? { pigment }
+
+ var pigment: Pigment? {
+ guard let color, !color.isEmpty else {
+ return nil
+ }
+
+ let _color = Pigment(color)
+ guard _color.alpha != 0.0 else {
+ return nil
+ }
+
+ return _color
+ }
+}
+
+public extension Stroke.LineCap {
+ var coreGraphicsDescription: String {
+ switch self {
+ case .butt: ".butt"
+ case .round: ".round"
+ case .square: ".square"
+ }
+ }
+}
+
+public extension Stroke.LineJoin {
+ var coreGraphicsDescription: String {
+ switch self {
+ case .bevel: ".bevel"
+ case .arcs, .miter, .miterClip: ".miter"
+ case .round: ".round"
+ }
+ }
+}
diff --git a/third-party/VectorPlus/Sources/Template+UIImageView.swift b/third-party/VectorPlus/Sources/Template+UIImageView.swift
new file mode 100644
index 0000000000..a6b03c8f47
--- /dev/null
+++ b/third-party/VectorPlus/Sources/Template+UIImageView.swift
@@ -0,0 +1,177 @@
+let imageViewSubclassTemplate: String = """
+#if canImport(UIKit)
+import UIKit
+
+@IBDesignable
+public class {{name}}: UIImageView {
+
+ public static let width: CGFloat = {{width}}
+ public static let height: CGFloat = {{height}}
+ public let width: CGFloat = {{width}}
+ public let height: CGFloat = {{height}}
+
+ public var widthToHeightAspectRatio: CGFloat {
+ guard width != .nan, width > 0.0 else {
+ return 0.0
+ }
+
+ guard height != .nan, height > 0.0 else {
+ return 0.0
+ }
+
+ return width / height
+ }
+
+ public var heightToWidthAspectRatio: CGFloat {
+ guard height != .nan, height > 0.0 else {
+ return 0.0
+ }
+
+ guard width != .nan, width > 0.0 else {
+ return 0.0
+ }
+
+ return height / width
+ }
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ updateSubviews()
+ }
+
+ public required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ updateSubviews()
+ }
+
+ public override var intrinsicContentSize: CGSize {
+ return CGSize(width: width, height: height)
+ }
+
+ public override var bounds: CGRect {
+ didSet {
+ updateSubviews()
+ }
+ }
+
+ public override func prepareForInterfaceBuilder() {
+ super.prepareForInterfaceBuilder()
+ updateSubviews()
+ }
+
+ public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+ super.traitCollectionDidChange(previousTraitCollection)
+ updateSubviews()
+ }
+
+ public func updateSubviews() {
+ image = Self.image(size: bounds.size)
+ }
+
+ public static func path(size: CGSize) -> CGPath {
+ guard size.height > 0.0 && size.width > 0.0 else {
+ return CGMutablePath()
+ }
+
+ let radius = max(size.width / 2.0, size.height / 2.0)
+ let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
+
+ let path = CGMutablePath()
+ {{instructions}}
+ return path
+ }
+
+ public static func image(size: CGSize) -> UIImage? {
+ guard size.height > 0.0 && size.width > 0.0 else {
+ return nil
+ }
+
+ let radius = max(size.width / 2.0, size.height / 2.0)
+ let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
+
+ defer {
+ UIGraphicsEndImageContext()
+ }
+
+ UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
+
+ guard let context = UIGraphicsGetCurrentContext() else {
+ return nil
+ }
+
+ {{render}}
+
+ return UIGraphicsGetImageFromCurrentImageContext()
+ }
+
+ private static func radians(_ degree: Float) -> CGFloat {
+ return CGFloat(degree) * (.pi / CGFloat(180))
+ }
+}
+
+private extension CGContext {
+ func rendering(_ block: (CGContext) -> Void) {
+ block(self)
+ }
+}
+
+#endif
+
+"""
+
+let contextTemplate: String = """
+ context.rendering { (ctx) in
+ ctx.saveGState()
+
+ let path = CGMutablePath()
+ {{instructions}}
+
+ let defaultColor: CGColor = CGColor(srgbRed: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
+ let pathFillColor: CGColor? = {{fillColor}}
+ let pathFillOpacity: CGFloat? = {{fillOpacity}}
+ let pathFillRule: CGPathFillRule = {{fillRule}}
+ let pathStrokeColor: CGColor? = {{strokeColor}}
+ let pathStrokeOpacity: CGFloat? = {{strokeOpacity}}
+ let pathStrokeWidth: CGFloat? = {{strokeWidth}}
+ let pathStrokeLineCap: CGLineCap? = {{strokeLineCap}}
+ let pathStrokeLineJoin: CGLineJoin? = {{strokeLineJoin}}
+ let pathStrokeMiterLimit: CGFloat? = {{strokeMiterLimit}}
+
+ if pathFillColor != nil && pathFillOpacity != nil {
+ let opacity = pathFillOpacity ?? 1.0
+ let color = (pathFillColor ?? defaultColor).copy(alpha: opacity) ?? defaultColor
+
+ ctx.setFillColor(color)
+ ctx.addPath(path)
+ ctx.fillPath(using: pathFillRule)
+ }
+
+ if pathStrokeColor != nil && pathStrokeOpacity != nil {
+ let opacity = pathStrokeOpacity ?? 1.0
+ let color = (pathStrokeColor ?? defaultColor).copy(alpha: opacity) ?? defaultColor
+ let lineWidth = pathStrokeWidth ?? 1.0
+
+ ctx.setLineWidth(lineWidth)
+ ctx.setStrokeColor(color)
+ if let lineCap = pathStrokeLineCap {
+ ctx.setLineCap(lineCap)
+ }
+ if let lineJoin = pathStrokeLineJoin {
+ ctx.setLineJoin(lineJoin)
+ if let miterLimit = pathStrokeMiterLimit, lineJoin == .miter {
+ ctx.setMiterLimit(miterLimit)
+ }
+ }
+ ctx.addPath(path)
+ ctx.strokePath()
+ }
+
+ if (pathFillColor == nil && pathFillOpacity == nil) && (pathStrokeColor == nil && pathStrokeOpacity == nil) {
+ ctx.setFillColor(defaultColor)
+ ctx.addPath(path)
+ ctx.fillPath(using: pathFillRule)
+ }
+
+ ctx.restoreGState()
+ }
+"""
diff --git a/third-party/VectorPlus/Sources/UIKit/SVG+UIImage.swift b/third-party/VectorPlus/Sources/UIKit/SVG+UIImage.swift
new file mode 100644
index 0000000000..ea336be4f7
--- /dev/null
+++ b/third-party/VectorPlus/Sources/UIKit/SVG+UIImage.swift
@@ -0,0 +1,43 @@
+#if canImport(UIKit)
+import Swift2D
+import SwiftSVG
+import UIKit
+
+public extension SVG {
+ func uiImage(size: Size) -> UIImage? {
+ guard size.height > 0.0, size.width > 0.0 else {
+ return nil
+ }
+
+ let from = Rect(origin: .zero, size: originalSize)
+ let to = Rect(origin: .zero, size: size)
+
+ let paths: [Path]
+ do {
+ paths = try subpaths()
+ } catch {
+ return nil
+ }
+
+ defer {
+ UIGraphicsEndImageContext()
+ }
+
+ UIGraphicsBeginImageContextWithOptions(CGSize(size), false, 0.0)
+
+ guard let context = UIGraphicsGetCurrentContext() else {
+ return nil
+ }
+
+ for path in paths {
+ try? context.render(path: path, from: from, to: to)
+ }
+
+ return UIGraphicsGetImageFromCurrentImageContext()
+ }
+
+ func pngData(size: Size) -> Data? {
+ uiImage(size: size)?.pngData()
+ }
+}
+#endif
diff --git a/third-party/VectorPlus/Sources/UIKit/SVGImageView.swift b/third-party/VectorPlus/Sources/UIKit/SVGImageView.swift
new file mode 100644
index 0000000000..b5131c915a
--- /dev/null
+++ b/third-party/VectorPlus/Sources/UIKit/SVGImageView.swift
@@ -0,0 +1,81 @@
+import Swift2D
+import SwiftSVG
+#if canImport(UIKit) && !os(watchOS)
+import UIKit
+
+@IBDesignable open class SVGImageView: UIImageView {
+
+ public var width: CGFloat {
+ CGFloat(svg.originalSize.width)
+ }
+
+ public var height: CGFloat {
+ CGFloat(svg.originalSize.height)
+ }
+
+ open var svg: SVG = SVG() {
+ didSet {
+ updateSubviews()
+ }
+ }
+
+ public var widthToHeightAspectRatio: CGFloat {
+ guard !width.isNaN, width > 0.0 else {
+ return 0.0
+ }
+
+ guard !height.isNaN, height > 0.0 else {
+ return 0.0
+ }
+
+ return width / height
+ }
+
+ public var heightToWidthAspectRatio: CGFloat {
+ guard !height.isNaN, height > 0.0 else {
+ return 0.0
+ }
+
+ guard !width.isNaN, width > 0.0 else {
+ return 0.0
+ }
+
+ return height / width
+ }
+
+ override public init(frame: CGRect) {
+ super.init(frame: frame)
+ updateSubviews()
+ }
+
+ public required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ updateSubviews()
+ }
+
+ override public var intrinsicContentSize: CGSize {
+ CGSize(width: width, height: height)
+ }
+
+ override public var bounds: CGRect {
+ didSet {
+ updateSubviews()
+ }
+ }
+
+ override public func prepareForInterfaceBuilder() {
+ super.prepareForInterfaceBuilder()
+ updateSubviews()
+ }
+
+ override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+ super.traitCollectionDidChange(previousTraitCollection)
+ updateSubviews()
+ }
+
+ public func updateSubviews() {
+ image = svg.uiImage(size: Size(bounds.size))
+ }
+}
+
+#endif
diff --git a/third-party/VectorPlus/Sources/VectorPoint.swift b/third-party/VectorPlus/Sources/VectorPoint.swift
new file mode 100644
index 0000000000..91f81b1d20
--- /dev/null
+++ b/third-party/VectorPlus/Sources/VectorPoint.swift
@@ -0,0 +1,126 @@
+import Swift2D
+
+/// A cartesian-based struct that describes the relationship of any particular `Point` to the _origin_ of a `Rect`.
+public struct VectorPoint {
+ public enum Sign: String {
+ case plus = "+"
+ case minus = "-"
+ }
+
+ public typealias Offset = (sign: Sign, multiplier: Double)
+
+ public var x: Offset
+ public var y: Offset
+
+ public init(x: Offset, y: Offset) {
+ self.x = x
+ self.y = y
+ }
+
+ /// Initializes a `VectorPoint` for a given `Point` container in the provided `Rect`.
+ public init(point: Point, in rect: Rect) {
+ let radius = rect.size.maxRadius
+ let cartesianPoint = Self.cartesianPoint(for: point, in: rect)
+
+ if cartesianPoint.x < 0 {
+ x = (.minus, abs(cartesianPoint.x) / radius)
+ } else {
+ x = (.plus, cartesianPoint.x / radius)
+ }
+
+ if cartesianPoint.y < 0 {
+ y = (.plus, abs(cartesianPoint.y) / radius)
+ } else {
+ y = (.minus, cartesianPoint.y / radius)
+ }
+ }
+}
+
+// MARK: - CustomStringConvertible
+
+extension VectorPoint: CustomStringConvertible {
+ public var description: String {
+ "VectorPoint(x: (\(x.sign.rawValue), \(x.multiplier)), y: (\(y.sign.rawValue), \(y.multiplier)))"
+ }
+}
+
+// MARK: - Equatable
+
+extension VectorPoint: Equatable {
+ public static func == (lhs: VectorPoint, rhs: VectorPoint) -> Bool {
+ guard lhs.x.sign == rhs.x.sign else {
+ return false
+ }
+ guard lhs.x.multiplier == rhs.x.multiplier else {
+ return false
+ }
+ guard lhs.y.sign == rhs.y.sign else {
+ return false
+ }
+ guard lhs.y.multiplier == rhs.y.multiplier else {
+ return false
+ }
+
+ return true
+ }
+}
+
+// MARK: -
+
+public extension VectorPoint {
+ /// Translates the provided point within the `Rect` from using the top-left
+ /// as the _origin_, to using the center as the _origin_.
+ ///
+ /// For example: Given `Rect(x: 0, y: 0, width: 100, height: 100)`, the point
+ /// `Point(x: 25, y: 25)` would translate to `Point(x: -25, y: 25)`.
+ static func cartesianPoint(for point: Point, in rect: Rect) -> Point {
+ let origin = Point(x: rect.size.width / 2.0, y: rect.size.height / 2.0)
+ var cartesianPoint: Point = .zero
+
+ if point.x < origin.x {
+ cartesianPoint = cartesianPoint.x(-(origin.x - point.x))
+ } else if point.x > origin.x {
+ cartesianPoint = cartesianPoint.x(point.x - origin.x)
+ }
+
+ if point.y > origin.y {
+ cartesianPoint = cartesianPoint.y(-(point.y - origin.y))
+ } else if point.y < origin.y {
+ cartesianPoint = cartesianPoint.y(origin.y - point.y)
+ }
+
+ return cartesianPoint
+ }
+}
+
+// MARK: - Instance Functionality
+
+public extension VectorPoint {
+ /// Calculates the `Point` for this instance in the specified `Rect`.
+ func translate(to rect: Rect) -> Point {
+ translate(to: rect.size)
+ }
+
+ /// Calculates the `Point` in the desired output size
+ func translate(to outputSize: Size) -> Point {
+ let center = outputSize.center
+ let radius = outputSize.minRadius
+
+ switch (x.sign, y.sign) {
+ case (.plus, .plus):
+ return Point(x: center.x + (radius * x.multiplier), y: center.y + (radius * y.multiplier))
+ case (.plus, .minus):
+ return Point(x: center.x + (radius * x.multiplier), y: center.y - (radius * y.multiplier))
+ case (.minus, .plus):
+ return Point(x: center.x - (radius * x.multiplier), y: center.y + (radius * y.multiplier))
+ case (.minus, .minus):
+ return Point(x: center.x - (radius * x.multiplier), y: center.y - (radius * y.multiplier))
+ }
+ }
+}
+
+public extension VectorPoint {
+ var coreGraphicsDescription: String {
+ "CGPoint(x: center.x \(x.sign.rawValue) (radius * \(x.multiplier)), y: center.y \(y.sign.rawValue) (radius * \(y.multiplier)))"
+ }
+}
diff --git a/third-party/XMLCoder/BUILD b/third-party/XMLCoder/BUILD
new file mode 100644
index 0000000000..13656e4a0c
--- /dev/null
+++ b/third-party/XMLCoder/BUILD
@@ -0,0 +1,17 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "XMLCoder",
+ module_name = "XMLCoder",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-suppress-warnings",
+ ],
+ deps = [
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Attribute.swift b/third-party/XMLCoder/Sources/Auxiliaries/Attribute.swift
new file mode 100644
index 0000000000..a908bee045
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Attribute.swift
@@ -0,0 +1,99 @@
+//
+// XMLAttribute.swift
+// XMLCoder
+//
+// Created by Benjamin Wetherfield on 6/3/20.
+//
+
+protocol XMLAttributeProtocol {}
+
+/** Property wrapper specifying that a given property should be encoded and decoded as an XML attribute.
+
+ For example, this type
+ ```swift
+ struct Book: Codable {
+ @Attribute var id: Int
+ }
+ ```
+
+ will encode value `Book(id: 42)` as ``. And vice versa,
+ it will decode the former into the latter.
+ */
+@propertyWrapper
+public struct Attribute: XMLAttributeProtocol {
+ public var wrappedValue: Value
+
+ public init(_ wrappedValue: Value) {
+ self.wrappedValue = wrappedValue
+ }
+}
+
+extension Attribute: Codable where Value: Codable {
+ public func encode(to encoder: Encoder) throws {
+ try wrappedValue.encode(to: encoder)
+ }
+
+ public init(from decoder: Decoder) throws {
+ try wrappedValue = .init(from: decoder)
+ }
+}
+
+extension Attribute: Equatable where Value: Equatable {}
+extension Attribute: Hashable where Value: Hashable {}
+extension Attribute: Sendable where Value: Sendable {}
+
+extension Attribute: ExpressibleByIntegerLiteral where Value: ExpressibleByIntegerLiteral {
+ public typealias IntegerLiteralType = Value.IntegerLiteralType
+
+ public init(integerLiteral value: Value.IntegerLiteralType) {
+ wrappedValue = Value(integerLiteral: value)
+ }
+}
+
+extension Attribute: ExpressibleByUnicodeScalarLiteral where Value: ExpressibleByUnicodeScalarLiteral {
+ public init(unicodeScalarLiteral value: Value.UnicodeScalarLiteralType) {
+ wrappedValue = Value(unicodeScalarLiteral: value)
+ }
+
+ public typealias UnicodeScalarLiteralType = Value.UnicodeScalarLiteralType
+}
+
+extension Attribute: ExpressibleByExtendedGraphemeClusterLiteral where Value: ExpressibleByExtendedGraphemeClusterLiteral {
+ public typealias ExtendedGraphemeClusterLiteralType = Value.ExtendedGraphemeClusterLiteralType
+
+ public init(extendedGraphemeClusterLiteral value: Value.ExtendedGraphemeClusterLiteralType) {
+ wrappedValue = Value(extendedGraphemeClusterLiteral: value)
+ }
+}
+
+extension Attribute: ExpressibleByStringLiteral where Value: ExpressibleByStringLiteral {
+ public typealias StringLiteralType = Value.StringLiteralType
+
+ public init(stringLiteral value: Value.StringLiteralType) {
+ wrappedValue = Value(stringLiteral: value)
+ }
+}
+
+extension Attribute: ExpressibleByBooleanLiteral where Value: ExpressibleByBooleanLiteral {
+ public typealias BooleanLiteralType = Value.BooleanLiteralType
+
+ public init(booleanLiteral value: Value.BooleanLiteralType) {
+ wrappedValue = Value(booleanLiteral: value)
+ }
+}
+
+extension Attribute: ExpressibleByNilLiteral where Value: ExpressibleByNilLiteral {
+ public init(nilLiteral: ()) {
+ wrappedValue = Value(nilLiteral: ())
+ }
+}
+
+protocol XMLOptionalAttributeProtocol: XMLAttributeProtocol {
+ init()
+}
+
+extension Attribute: XMLOptionalAttributeProtocol where Value: AnyOptional {
+ init() {
+ wrappedValue = Value()
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/BoolBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/BoolBox.swift
new file mode 100644
index 0000000000..5997804eda
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/BoolBox.swift
@@ -0,0 +1,53 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/17/18.
+//
+
+struct BoolBox: Equatable {
+ typealias Unboxed = Bool
+
+ let unboxed: Unboxed
+
+ init(_ unboxed: Unboxed) {
+ self.unboxed = unboxed
+ }
+
+ init?(xmlString: String) {
+ switch xmlString.lowercased() {
+ case "false", "0", "n", "no": self.init(false)
+ case "true", "1", "y", "yes": self.init(true)
+ case _: return nil
+ }
+ }
+}
+
+extension BoolBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ /// # Lexical representation
+ /// Boolean has a lexical representation consisting of the following
+ /// legal literals {`true`, `false`, `1`, `0`}.
+ ///
+ /// # Canonical representation
+ /// The canonical representation for boolean is the set of literals {`true`, `false`}.
+ ///
+ /// ---
+ ///
+ /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#boolean)
+ var xmlString: String? {
+ return (unboxed) ? "true" : "false"
+ }
+}
+
+extension BoolBox: SimpleBox {}
+
+extension BoolBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/Box.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/Box.swift
new file mode 100644
index 0000000000..71ecf82267
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/Box.swift
@@ -0,0 +1,32 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/17/18.
+//
+
+protocol Box {
+ var isNull: Bool { get }
+ var xmlString: String? { get }
+}
+
+/// A box that only describes a single atomic value.
+protocol SimpleBox: Box {
+ // A simple tagging protocol, for now.
+}
+
+protocol TypeErasedSharedBoxProtocol {
+ func typeErasedUnbox() -> Box
+}
+
+protocol SharedBoxProtocol: TypeErasedSharedBoxProtocol {
+ associatedtype B: Box
+ func unbox() -> B
+}
+
+extension SharedBoxProtocol {
+ func typeErasedUnbox() -> Box {
+ return unbox()
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/ChoiceBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/ChoiceBox.swift
new file mode 100644
index 0000000000..4d2f9b0289
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/ChoiceBox.swift
@@ -0,0 +1,41 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by James Bean on 7/18/19.
+//
+
+/// A `Box` which represents an element which is known to contain an XML choice element.
+struct ChoiceBox {
+ var key: String = ""
+ var element: Box = NullBox()
+}
+
+extension ChoiceBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ var xmlString: String? {
+ return nil
+ }
+}
+
+extension ChoiceBox: SimpleBox {}
+
+extension ChoiceBox {
+ init?(_ keyedBox: KeyedBox) {
+ guard
+ let firstKey = keyedBox.elements.keys.first,
+ let firstElement = keyedBox.elements[firstKey].first
+ else {
+ return nil
+ }
+ self.init(key: firstKey, element: firstElement)
+ }
+
+ init(_ singleKeyedBox: SingleKeyedBox) {
+ self.init(key: singleKeyedBox.key, element: singleKeyedBox.element)
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/DataBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/DataBox.swift
new file mode 100644
index 0000000000..dfa7f94217
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/DataBox.swift
@@ -0,0 +1,57 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/19/18.
+//
+
+import Foundation
+
+struct DataBox: Equatable {
+ enum Format: Equatable {
+ case base64
+ }
+
+ typealias Unboxed = Data
+
+ let unboxed: Unboxed
+ let format: Format
+
+ init(_ unboxed: Unboxed, format: Format) {
+ self.unboxed = unboxed
+ self.format = format
+ }
+
+ init?(base64 string: String) {
+ guard let data = Data(base64Encoded: string) else {
+ return nil
+ }
+ self.init(data, format: .base64)
+ }
+
+ func xmlString(format: Format) -> String {
+ switch format {
+ case .base64:
+ return unboxed.base64EncodedString()
+ }
+ }
+}
+
+extension DataBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ var xmlString: String? {
+ return xmlString(format: format)
+ }
+}
+
+extension DataBox: SimpleBox {}
+
+extension DataBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/DateBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/DateBox.swift
new file mode 100644
index 0000000000..c18957b569
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/DateBox.swift
@@ -0,0 +1,99 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/18/18.
+//
+
+import Foundation
+
+struct DateBox: Equatable, Sendable {
+ enum Format: Equatable {
+ case secondsSince1970
+ case millisecondsSince1970
+ case iso8601
+ case formatter(DateFormatter)
+ }
+
+ typealias Unboxed = Date
+
+ let unboxed: Unboxed
+ let format: Format
+
+ init(_ unboxed: Unboxed, format: Format) {
+ self.unboxed = unboxed
+ self.format = format
+ }
+
+ init?(secondsSince1970 string: String) {
+ guard let seconds = TimeInterval(string) else {
+ return nil
+ }
+ let unboxed = Date(timeIntervalSince1970: seconds)
+ self.init(unboxed, format: .secondsSince1970)
+ }
+
+ init?(millisecondsSince1970 string: String) {
+ guard let milliseconds = TimeInterval(string) else {
+ return nil
+ }
+ let unboxed = Date(timeIntervalSince1970: milliseconds / 1000.0)
+ self.init(unboxed, format: .millisecondsSince1970)
+ }
+
+ init?(iso8601 string: String) {
+ if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
+ guard let unboxed = ISO8601DateFormatter.xmlCoderFormatter().date(from: string) else {
+ return nil
+ }
+ self.init(unboxed, format: .iso8601)
+ } else {
+ fatalError("ISO8601DateFormatter is unavailable on this platform.")
+ }
+ }
+
+ init?(xmlString: String, formatter: DateFormatter) {
+ guard let date = formatter.date(from: xmlString) else {
+ return nil
+ }
+ self.init(date, format: .formatter(formatter))
+ }
+
+ func xmlString(format: Format) -> String {
+ switch format {
+ case .secondsSince1970:
+ let seconds = unboxed.timeIntervalSince1970
+ return seconds.description
+ case .millisecondsSince1970:
+ let milliseconds = unboxed.timeIntervalSince1970 * 1000.0
+ return milliseconds.description
+ case .iso8601:
+ if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
+ return ISO8601DateFormatter.xmlCoderFormatter().string(from: self.unboxed)
+ } else {
+ fatalError("ISO8601DateFormatter is unavailable on this platform.")
+ }
+ case let .formatter(formatter):
+ return formatter.string(from: unboxed)
+ }
+ }
+}
+
+extension DateBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ var xmlString: String? {
+ return xmlString(format: format)
+ }
+}
+
+extension DateBox: SimpleBox {}
+
+extension DateBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/DecimalBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/DecimalBox.swift
new file mode 100644
index 0000000000..35e09443da
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/DecimalBox.swift
@@ -0,0 +1,62 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/17/18.
+//
+
+import Foundation
+
+struct DecimalBox: Equatable {
+ typealias Unboxed = Decimal
+
+ let unboxed: Unboxed
+
+ init(_ unboxed: Unboxed) {
+ self.unboxed = unboxed
+ }
+
+ init?(xmlString: String) {
+ guard let unboxed = Unboxed(string: xmlString) else {
+ return nil
+ }
+ self.init(unboxed)
+ }
+}
+
+extension DecimalBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ /// # Lexical representation
+ /// Decimal has a lexical representation consisting of a finite-length sequence of
+ /// decimal digits separated by a period as a decimal indicator.
+ /// An optional leading sign is allowed. If the sign is omitted, `"+"` is assumed.
+ /// Leading and trailing zeroes are optional. If the fractional part is zero,
+ /// the period and following zero(es) can be omitted.
+ /// For example: `-1.23`, `12678967.543233`, `+100000.00`, `210`.
+ ///
+ /// # Canonical representation
+ /// The canonical representation for decimal is defined by prohibiting certain
+ /// options from the Lexical representation. Specifically, the preceding optional
+ /// `"+"` sign is prohibited. The decimal point is required. Leading and trailing
+ /// zeroes are prohibited subject to the following: there must be at least one
+ /// digit to the right and to the left of the decimal point which may be a zero.
+ ///
+ /// ---
+ ///
+ /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#decimal)
+ var xmlString: String? {
+ return "\(unboxed)"
+ }
+}
+
+extension DecimalBox: SimpleBox {}
+
+extension DecimalBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/DoubleBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/DoubleBox.swift
new file mode 100644
index 0000000000..42302608d0
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/DoubleBox.swift
@@ -0,0 +1,49 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Max Desiatov on 05/10/2019.
+//
+
+struct DoubleBox: Equatable, ValueBox {
+ typealias Unboxed = Double
+
+ let unboxed: Unboxed
+
+ init(_ value: Unboxed) {
+ unboxed = value
+ }
+
+ init?(xmlString: String) {
+ guard let unboxed = Double(xmlString) else { return nil }
+
+ self.init(unboxed)
+ }
+}
+
+extension DoubleBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ var xmlString: String? {
+ guard !unboxed.isNaN else {
+ return "NaN"
+ }
+
+ guard !unboxed.isInfinite else {
+ return (unboxed > 0.0) ? "INF" : "-INF"
+ }
+
+ return unboxed.description
+ }
+}
+
+extension DoubleBox: SimpleBox {}
+
+extension DoubleBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/FloatBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/FloatBox.swift
new file mode 100644
index 0000000000..3f0e283af7
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/FloatBox.swift
@@ -0,0 +1,78 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/17/18.
+//
+
+struct FloatBox: Equatable, ValueBox {
+ typealias Unboxed = Float
+
+ let unboxed: Unboxed
+
+ init(_ unboxed: Float) {
+ self.unboxed = Unboxed(unboxed)
+ }
+
+ init?(xmlString: String) {
+ guard let unboxed = Unboxed(xmlString) else {
+ return nil
+ }
+ self.init(unboxed)
+ }
+}
+
+extension FloatBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ /// # Lexical representation
+ /// float values have a lexical representation consisting of a mantissa followed, optionally,
+ /// by the character `"E"` or `"e"`, followed by an exponent. The exponent **must** be an integer.
+ /// The mantissa **must** be a decimal number. The representations for exponent and mantissa **must**
+ /// follow the lexical rules for integer and decimal. If the `"E"` or `"e"` and the following
+ /// exponent are omitted, an exponent value of `0` is assumed.
+ ///
+ /// The special values positive and negative infinity and not-a-number have lexical
+ /// representations `INF`, `-INF` and `NaN`, respectively. Lexical representations for zero
+ /// may take a positive or negative sign.
+ ///
+ /// For example, `-1E4`, `1267.43233E12`, `12.78e-2`, `12` , `-0`, `0` and `INF` are all
+ /// legal literals for float.
+ ///
+ /// # Canonical representation
+ /// The canonical representation for float is defined by prohibiting certain options from the
+ /// Lexical representation. Specifically, the exponent must be indicated by `"E"`.
+ /// Leading zeroes and the preceding optional `"+"` sign are prohibited in the exponent.
+ /// If the exponent is zero, it must be indicated by `"E0"`. For the mantissa, the preceding
+ /// optional `"+"` sign is prohibited and the decimal point is required. Leading and trailing
+ /// zeroes are prohibited subject to the following: number representations must be normalized
+ /// such that there is a single digit which is non-zero to the left of the decimal point and
+ /// at least a single digit to the right of the decimal point unless the value being represented
+ /// is zero. The canonical representation for zero is `0.0E0`.
+ ///
+ /// ---
+ ///
+ /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#float)
+ var xmlString: String? {
+ guard !unboxed.isNaN else {
+ return "NaN"
+ }
+
+ guard !unboxed.isInfinite else {
+ return (unboxed > 0.0) ? "INF" : "-INF"
+ }
+
+ return unboxed.description
+ }
+}
+
+extension FloatBox: SimpleBox {}
+
+extension FloatBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/IntBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/IntBox.swift
new file mode 100644
index 0000000000..35380a6455
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/IntBox.swift
@@ -0,0 +1,59 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/17/18.
+//
+
+struct IntBox: Equatable {
+ typealias Unboxed = Int64
+
+ let unboxed: Unboxed
+
+ init(_ unboxed: Integer) {
+ self.unboxed = Unboxed(unboxed)
+ }
+
+ init?(xmlString: String) {
+ guard let unboxed = Unboxed(xmlString) else {
+ return nil
+ }
+ self.init(unboxed)
+ }
+
+ func unbox() -> Integer? {
+ return Integer(exactly: unboxed)
+ }
+}
+
+extension IntBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ /// # Lexical representation
+ /// Integer has a lexical representation consisting of a finite-length sequence of
+ /// decimal digits with an optional leading sign. If the sign is omitted, `"+"` is assumed.
+ /// For example: `-1`, `0`, `12678967543233`, `+100000`.
+ ///
+ /// # Canonical representation
+ /// The canonical representation for integer is defined by prohibiting certain
+ /// options from the Lexical representation. Specifically, the preceding optional
+ /// `"+"` sign is prohibited and leading zeroes are prohibited.
+ ///
+ /// ---
+ ///
+ /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#integer)
+ var xmlString: String? {
+ return unboxed.description
+ }
+}
+
+extension IntBox: SimpleBox {}
+
+extension IntBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/KeyedBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/KeyedBox.swift
new file mode 100644
index 0000000000..271f665d92
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/KeyedBox.swift
@@ -0,0 +1,57 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 11/19/18.
+//
+
+struct KeyedBox {
+ typealias Key = String
+ typealias Attribute = SimpleBox
+ typealias Element = Box
+
+ typealias Attributes = KeyedStorage
+ typealias Elements = KeyedStorage
+
+ var elements = Elements()
+ var attributes = Attributes()
+
+ var unboxed: (elements: Elements, attributes: Attributes) {
+ return (
+ elements: elements,
+ attributes: attributes
+ )
+ }
+
+ var value: SimpleBox? {
+ return elements.values.first as? SimpleBox
+ }
+}
+
+extension KeyedBox {
+ init(elements: E, attributes: A)
+ where E: Sequence, E.Element == (Key, Element),
+ A: Sequence, A.Element == (Key, Attribute)
+ {
+ let elements = Elements(elements)
+ let attributes = Attributes(attributes)
+ self.init(elements: elements, attributes: attributes)
+ }
+}
+
+extension KeyedBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ var xmlString: String? {
+ return nil
+ }
+}
+
+extension KeyedBox: CustomStringConvertible {
+ var description: String {
+ return "{attributes: \(attributes), elements: \(elements)}"
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/NullBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/NullBox.swift
new file mode 100644
index 0000000000..e5dd66b517
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/NullBox.swift
@@ -0,0 +1,33 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/17/18.
+//
+
+struct NullBox {}
+
+extension NullBox: Box {
+ var isNull: Bool {
+ return true
+ }
+
+ var xmlString: String? {
+ return nil
+ }
+}
+
+extension NullBox: SimpleBox {}
+
+extension NullBox: Equatable {
+ static func ==(_: NullBox, _: NullBox) -> Bool {
+ return true
+ }
+}
+
+extension NullBox: CustomStringConvertible {
+ var description: String {
+ return "null"
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/SharedBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/SharedBox.swift
new file mode 100644
index 0000000000..d05af76f8d
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/SharedBox.swift
@@ -0,0 +1,35 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/22/18.
+//
+
+class SharedBox {
+ private(set) var unboxed: Unboxed
+
+ init(_ wrapped: Unboxed) {
+ unboxed = wrapped
+ }
+
+ func withShared(_ body: (inout Unboxed) throws -> T) rethrows -> T {
+ return try body(&unboxed)
+ }
+}
+
+extension SharedBox: Box {
+ var isNull: Bool {
+ return unboxed.isNull
+ }
+
+ var xmlString: String? {
+ return unboxed.xmlString
+ }
+}
+
+extension SharedBox: SharedBoxProtocol {
+ func unbox() -> Unboxed {
+ return unboxed
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/SingleKeyedBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/SingleKeyedBox.swift
new file mode 100644
index 0000000000..47c6370d56
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/SingleKeyedBox.swift
@@ -0,0 +1,25 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by James Bean on 7/15/19.
+//
+
+/// A `Box` which contains a single `key` and `element` pair. This is useful for disambiguating elements which could either represent
+/// an element nested in a keyed or unkeyed container, or an choice between multiple known-typed values (implemented in Swift using
+/// enums with associated values).
+struct SingleKeyedBox: SimpleBox {
+ var key: String
+ var element: Box
+}
+
+extension SingleKeyedBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ var xmlString: String? {
+ return nil
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/StringBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/StringBox.swift
new file mode 100644
index 0000000000..c33629c99a
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/StringBox.swift
@@ -0,0 +1,39 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/17/18.
+//
+
+struct StringBox: Equatable {
+ typealias Unboxed = String
+
+ let unboxed: Unboxed
+
+ init(_ unboxed: Unboxed) {
+ self.unboxed = unboxed
+ }
+
+ init(xmlString: Unboxed) {
+ self.init(xmlString)
+ }
+}
+
+extension StringBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ var xmlString: String? {
+ return unboxed.description
+ }
+}
+
+extension StringBox: SimpleBox {}
+
+extension StringBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/UIntBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/UIntBox.swift
new file mode 100644
index 0000000000..2fd70ac156
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/UIntBox.swift
@@ -0,0 +1,62 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/17/18.
+//
+
+struct UIntBox: Equatable {
+ typealias Unboxed = UInt64
+
+ let unboxed: Unboxed
+
+ init(_ unboxed: Integer) {
+ self.unboxed = Unboxed(unboxed)
+ }
+
+ init?(xmlString: String) {
+ guard let unboxed = Unboxed(xmlString) else {
+ return nil
+ }
+ self.init(unboxed)
+ }
+
+ func unbox() -> Integer? {
+ return Integer(exactly: unboxed)
+ }
+}
+
+extension UIntBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ /// # Lexical representation
+ /// Unsigned integer has a lexical representation consisting of an optional
+ /// sign followed by a finite-length sequence of decimal digits.
+ /// If the sign is omitted, the positive sign (`"+"`) is assumed.
+ /// If the sign is present, it must be `"+"` except for lexical forms denoting zero,
+ /// which may be preceded by a positive (`"+"`) or a negative (`"-"`) sign.
+ /// For example: `1`, `0`, `12678967543233`, `+100000`.
+ ///
+ /// # Canonical representation
+ /// The canonical representation for nonNegativeInteger is defined by prohibiting
+ /// certain options from the Lexical representation. Specifically,
+ /// the the optional `"+"` sign is prohibited and leading zeroes are prohibited.
+ ///
+ /// ---
+ ///
+ /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#nonNegativeInteger)
+ var xmlString: String? {
+ return unboxed.description
+ }
+}
+
+extension UIntBox: SimpleBox {}
+
+extension UIntBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/URLBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/URLBox.swift
new file mode 100644
index 0000000000..6c12061783
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/URLBox.swift
@@ -0,0 +1,44 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/21/18.
+//
+
+import Foundation
+
+struct URLBox: Equatable {
+ typealias Unboxed = URL
+
+ let unboxed: Unboxed
+
+ init(_ unboxed: Unboxed) {
+ self.unboxed = unboxed
+ }
+
+ init?(xmlString: String) {
+ guard let unboxed = Unboxed(string: xmlString) else {
+ return nil
+ }
+ self.init(unboxed)
+ }
+}
+
+extension URLBox: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ var xmlString: String? {
+ return unboxed.absoluteString
+ }
+}
+
+extension URLBox: SimpleBox {}
+
+extension URLBox: CustomStringConvertible {
+ var description: String {
+ return unboxed.description
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/UnkeyedBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/UnkeyedBox.swift
new file mode 100644
index 0000000000..203a21674c
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/UnkeyedBox.swift
@@ -0,0 +1,19 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 11/20/18.
+//
+
+typealias UnkeyedBox = [Box]
+
+extension Array: Box {
+ var isNull: Bool {
+ return false
+ }
+
+ var xmlString: String? {
+ return nil
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/ValueBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/ValueBox.swift
new file mode 100644
index 0000000000..a6739c471d
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/ValueBox.swift
@@ -0,0 +1,13 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Max Desiatov on 05/10/2019.
+//
+
+protocol ValueBox: SimpleBox {
+ associatedtype Unboxed
+
+ init(_ value: Unboxed)
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Element.swift b/third-party/XMLCoder/Sources/Auxiliaries/Element.swift
new file mode 100644
index 0000000000..8728e4ac74
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Element.swift
@@ -0,0 +1,43 @@
+//
+// XMLElementNode.swift
+// XMLCoder
+//
+// Created by Benjamin Wetherfield on 6/4/20.
+//
+
+protocol XMLElementProtocol {}
+
+/** Property wrapper specifying that a given property should be encoded and decoded as an XML element.
+
+ For example, this type
+ ```swift
+ struct Book: Codable {
+ @Element var id: Int
+ }
+ ```
+
+ will encode value `Book(id: 42)` as `42`. And vice versa,
+ it will decode the former into the latter.
+ */
+@propertyWrapper
+public struct Element: XMLElementProtocol {
+ public var wrappedValue: Value
+
+ public init(_ wrappedValue: Value) {
+ self.wrappedValue = wrappedValue
+ }
+}
+
+extension Element: Codable where Value: Codable {
+ public func encode(to encoder: Encoder) throws {
+ try wrappedValue.encode(to: encoder)
+ }
+
+ public init(from decoder: Decoder) throws {
+ try wrappedValue = .init(from: decoder)
+ }
+}
+
+extension Element: Equatable where Value: Equatable {}
+extension Element: Hashable where Value: Hashable {}
+extension Element: Sendable where Value: Sendable {}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/ElementAndAttribute.swift b/third-party/XMLCoder/Sources/Auxiliaries/ElementAndAttribute.swift
new file mode 100644
index 0000000000..b04cfc5629
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/ElementAndAttribute.swift
@@ -0,0 +1,44 @@
+//
+// XMLBothNode.swift
+// XMLCoder
+//
+// Created by Benjamin Wetherfield on 6/7/20.
+//
+
+protocol XMLElementAndAttributeProtocol {}
+
+/** Property wrapper specifying that a given property should be decoded from either an XML element
+ or an XML attribute. When encoding, the value will be present as both an attribute, and an element.
+
+ For example, this type
+ ```swift
+ struct Book: Codable {
+ @ElementAndAttribute var id: Int
+ }
+ ```
+
+ will encode value `Book(id: 42)` as `42`. It will decode both
+ `42` and `` as `Book(id: 42)`.
+ */
+@propertyWrapper
+public struct ElementAndAttribute: XMLElementAndAttributeProtocol {
+ public var wrappedValue: Value
+
+ public init(_ wrappedValue: Value) {
+ self.wrappedValue = wrappedValue
+ }
+}
+
+extension ElementAndAttribute: Codable where Value: Codable {
+ public func encode(to encoder: Encoder) throws {
+ try wrappedValue.encode(to: encoder)
+ }
+
+ public init(from decoder: Decoder) throws {
+ try wrappedValue = .init(from: decoder)
+ }
+}
+
+extension ElementAndAttribute: Equatable where Value: Equatable {}
+extension ElementAndAttribute: Hashable where Value: Hashable {}
+extension ElementAndAttribute: Sendable where Value: Sendable {}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/ISO8601DateFormatter.swift b/third-party/XMLCoder/Sources/Auxiliaries/ISO8601DateFormatter.swift
new file mode 100644
index 0000000000..a431b23e64
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/ISO8601DateFormatter.swift
@@ -0,0 +1,17 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/21/17.
+//
+
+import Foundation
+
+extension ISO8601DateFormatter {
+ static func xmlCoderFormatter() -> ISO8601DateFormatter {
+ let formatter = ISO8601DateFormatter()
+ formatter.formatOptions = .withInternetDateTime
+ return formatter
+ }
+}
\ No newline at end of file
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/KeyedStorage.swift b/third-party/XMLCoder/Sources/Auxiliaries/KeyedStorage.swift
new file mode 100644
index 0000000000..808a38dce7
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/KeyedStorage.swift
@@ -0,0 +1,82 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Max Desiatov on 07/04/2019.
+//
+
+struct KeyedStorage {
+ typealias Buffer = [(Key, Value)]
+ typealias KeyMap = [Key: [Int]]
+
+ fileprivate var keyMap = KeyMap()
+ fileprivate var buffer = Buffer()
+
+ var isEmpty: Bool {
+ return buffer.isEmpty
+ }
+
+ var count: Int {
+ return buffer.count
+ }
+
+ var keys: [Key] {
+ return buffer.map { $0.0 }
+ }
+
+ var values: [Value] {
+ return buffer.map { $0.1 }
+ }
+
+ init(_ sequence: S) where S: Sequence, S.Element == (Key, Value) {
+ buffer = Buffer()
+ keyMap = KeyMap()
+ sequence.forEach { key, value in append(value, at: key) }
+ }
+
+ subscript(key: Key) -> [Value] {
+ return keyMap[key]?.map { buffer[$0].1 } ?? []
+ }
+
+ mutating func append(_ value: Value, at key: Key) {
+ let i = buffer.count
+ buffer.append((key, value))
+ if keyMap[key] != nil {
+ keyMap[key]?.append(i)
+ } else {
+ keyMap[key] = [i]
+ }
+ }
+
+ func map(_ transform: (Key, Value) throws -> T) rethrows -> [T] {
+ return try buffer.map(transform)
+ }
+
+ func compactMap(
+ _ transform: ((Key, Value)) throws -> T?
+ ) rethrows -> [T] {
+ return try buffer.compactMap(transform)
+ }
+
+ mutating func reserveCapacity(_ capacity: Int) {
+ buffer.reserveCapacity(capacity)
+ keyMap.reserveCapacity(capacity)
+ }
+
+ init() {}
+}
+
+extension KeyedStorage: Sequence {
+ func makeIterator() -> Buffer.Iterator {
+ return buffer.makeIterator()
+ }
+}
+
+extension KeyedStorage: CustomStringConvertible {
+ var description: String {
+ let result = buffer.map { "\"\($0)\": \($1)" }.joined(separator: ", ")
+
+ return "[\(result)]"
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Metatypes.swift b/third-party/XMLCoder/Sources/Auxiliaries/Metatypes.swift
new file mode 100644
index 0000000000..40be7f3322
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Metatypes.swift
@@ -0,0 +1,31 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Max Desiatov on 30/12/2018.
+//
+
+/// Type-erased protocol helper for a metatype check in generic `decode`
+/// overload. If you custom sequence type is not decoded correctly, try
+/// making it confirm to `XMLDecodableSequence`. Default conformances for
+/// `Array` and `Dictionary` are already provided by the XMLCoder library.
+public protocol XMLDecodableSequence {
+ init()
+}
+
+extension Array: XMLDecodableSequence {}
+
+extension Dictionary: XMLDecodableSequence {}
+
+/// Type-erased protocol helper for a metatype check in generic `decode`
+/// overload.
+protocol AnyOptional {
+ init()
+}
+
+extension Optional: AnyOptional {
+ init() {
+ self = nil
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/String+Extensions.swift b/third-party/XMLCoder/Sources/Auxiliaries/String+Extensions.swift
new file mode 100644
index 0000000000..59ce223f81
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/String+Extensions.swift
@@ -0,0 +1,52 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/18/18.
+//
+
+import Foundation
+
+extension StringProtocol where Self.Index == String.Index {
+ func escape(_ characterSet: [(character: String, escapedCharacter: String)]) -> String {
+ var string = String(self)
+
+ for set in characterSet {
+ string = string.replacingOccurrences(of: set.character, with: set.escapedCharacter, options: .literal)
+ }
+
+ return string
+ }
+}
+
+extension StringProtocol {
+ func capitalizingFirstLetter() -> Self {
+ guard !isEmpty else {
+ return self
+ }
+ return Self(prefix(1).uppercased() + dropFirst())!
+ }
+
+ mutating func capitalizeFirstLetter() {
+ self = capitalizingFirstLetter()
+ }
+
+ func lowercasingFirstLetter() -> Self {
+ // avoid lowercasing single letters (I), or capitalized multiples (AThing ! to aThing, leave as AThing)
+ guard count > 1, !(String(prefix(2)) == prefix(2).lowercased()) else {
+ return self
+ }
+ return Self(prefix(1).lowercased() + dropFirst())!
+ }
+
+ mutating func lowercaseFirstLetter() {
+ self = lowercasingFirstLetter()
+ }
+}
+
+extension String {
+ func isAllWhitespace() -> Bool {
+ return unicodeScalars.allSatisfy(CharacterSet.whitespacesAndNewlines.contains)
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Utils.swift b/third-party/XMLCoder/Sources/Auxiliaries/Utils.swift
new file mode 100644
index 0000000000..747f327f6e
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/Utils.swift
@@ -0,0 +1,13 @@
+// Copyright (c) 2018-2023 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Alkenso (Vladimir Vashurkin) on 08.06.2023.
+//
+
+import Foundation
+
+extension CodingKey {
+ internal var isInlined: Bool { stringValue == "" }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLChoiceCodingKey.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLChoiceCodingKey.swift
new file mode 100644
index 0000000000..aa3dbff25f
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLChoiceCodingKey.swift
@@ -0,0 +1,59 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Benjamin Wetherfield on 7/17/19.
+//
+
+/// An empty marker protocol that can be used in place of `CodingKey`. It must be used when
+/// attempting to encode and decode union-type–like enums with associated values to and from `XML`
+/// choice elements.
+///
+/// - Important: In order for your `XML`-destined `Codable` type to be encoded and/or decoded
+/// properly, you must conform your custom `CodingKey` type additionally to `XMLChoiceCodingKey`.
+///
+/// For example, say you have defined a type which can hold _either_ an `Int` _or_ a `String`:
+///
+/// enum IntOrString {
+/// case int(Int)
+/// case string(String)
+/// }
+///
+/// Implementing the requirements for the `Codable` protocol like this:
+///
+/// extension IntOrString: Codable {
+/// enum CodingKeys: String, XMLChoiceCodingKey {
+/// case int
+/// case string
+/// }
+///
+/// func encode(to encoder: Encoder) throws {
+/// var container = encoder.container(keyedBy: CodingKeys.self)
+/// switch self {
+/// case let .int(value):
+/// try container.encode(value, forKey: .int)
+/// case let .string(value):
+/// try container.encode(value, forKey: .string)
+/// }
+/// }
+///
+/// init(from decoder: Decoder) throws {
+/// let container = try decoder.container(keyedBy: CodingKeys.self)
+/// do {
+/// self = .int(try container.decode(Int.self, forKey: .int))
+/// } catch {
+/// self = .string(try container.decode(String.self, forKey: .string))
+/// }
+/// }
+/// }
+///
+/// Retroactively conform the `CodingKeys` enum to `XMLChoiceCodingKey` when targeting `XML` as your
+/// encoded format.
+///
+/// extension IntOrString.CodingKeys: XMLChoiceCodingKey {}
+///
+/// - Note: The `XMLChoiceCodingKey` marker protocol allows the `XMLEncoder` / `XMLDecoder` to
+/// resolve ambiguities particular to the `XML` format between nested unkeyed container elements and
+/// choice elements.
+public protocol XMLChoiceCodingKey: CodingKey {}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLCoderElement.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLCoderElement.swift
new file mode 100644
index 0000000000..1b1d8a0ffd
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLCoderElement.swift
@@ -0,0 +1,451 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/18/18.
+//
+
+import Foundation
+
+struct XMLCoderElement: Equatable, Sendable {
+ struct Attribute: Equatable, Sendable {
+ let key: String
+ let value: String
+ }
+
+ let key: String
+ private(set) var stringValue: String?
+ private(set) var elements: [XMLCoderElement] = []
+ private(set) var attributes: [Attribute] = []
+ private(set) var containsTextNodes: Bool = false
+
+ var isStringNode: Bool {
+ return key == ""
+ }
+
+ var isCDATANode: Bool {
+ return key == "#CDATA"
+ }
+
+ var isTextNode: Bool {
+ return isStringNode || isCDATANode
+ }
+
+ private var isInlined: Bool {
+ return key.isEmpty
+ }
+
+ init(
+ key: String,
+ elements: [XMLCoderElement] = [],
+ attributes: [Attribute] = []
+ ) {
+ self.key = key
+ stringValue = nil
+ self.elements = elements
+ self.attributes = attributes
+ }
+
+ init(
+ key: String,
+ stringValue string: String,
+ attributes: [Attribute] = []
+ ) {
+ self.key = key
+ elements = [XMLCoderElement(stringValue: string)]
+ self.attributes = attributes
+ containsTextNodes = true
+ }
+
+ init(
+ key: String,
+ cdataValue string: String,
+ attributes: [Attribute] = []
+ ) {
+ self.key = key
+ elements = [XMLCoderElement(cdataValue: string)]
+ self.attributes = attributes
+ containsTextNodes = true
+ }
+
+ init(stringValue string: String) {
+ key = ""
+ stringValue = string
+ }
+
+ init(cdataValue string: String) {
+ key = "#CDATA"
+ stringValue = string
+ }
+
+ mutating func append(element: XMLCoderElement) {
+ elements.append(element)
+ containsTextNodes = containsTextNodes || element.isTextNode
+ }
+
+ mutating func append(string: String) {
+ if elements.last?.isTextNode == true {
+ let oldValue = elements[elements.count - 1].stringValue ?? ""
+ elements[elements.count - 1].stringValue = oldValue + string
+ } else {
+ elements.append(XMLCoderElement(stringValue: string))
+ }
+ containsTextNodes = true
+ }
+
+ mutating func append(cdata string: String) {
+ if elements.last?.isCDATANode == true {
+ let oldValue = elements[elements.count - 1].stringValue ?? ""
+ elements[elements.count - 1].stringValue = oldValue + string
+ } else {
+ elements.append(XMLCoderElement(cdataValue: string))
+ }
+ containsTextNodes = true
+ }
+
+ mutating func trimTextNodes() {
+ guard containsTextNodes else { return }
+ for idx in elements.indices {
+ elements[idx].stringValue = elements[idx].stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+ }
+
+ func transformToBoxTree() -> Box {
+ if isTextNode {
+ return StringBox(stringValue!)
+ }
+
+ let attributes = KeyedStorage(self.attributes.map { attribute in
+ (key: attribute.key, value: StringBox(attribute.value) as SimpleBox)
+ })
+
+ var storage = KeyedStorage()
+ // storage.reserveCapacity(self.elements)
+
+ for element in self.elements {
+
+ let hasElements = !element.elements.isEmpty
+ let hasAttributes = !element.attributes.isEmpty
+ let hasText = element.stringValue != nil
+
+ if hasElements || hasAttributes {
+ storage.append(element.transformToBoxTree(), at: element.key)
+ } else if hasText {
+ storage.append(element.transformToBoxTree(), at: element.key)
+ } else {
+ storage.append(SingleKeyedBox(key: element.key, element: NullBox()), at: element.key)
+ }
+ }
+
+ return KeyedBox(elements: storage, attributes: attributes)
+ }
+
+ func toXMLString(
+ with header: XMLHeader?,
+ doctype: XMLDocumentType?,
+ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
+ formatting: XMLEncoder.OutputFormatting,
+ indentation: XMLEncoder.PrettyPrintIndentation
+ ) -> String {
+ var base = ""
+
+ if let header = header, let headerXML = header.toXML() {
+ base += headerXML
+ }
+
+ if let doctype = doctype {
+ base += doctype.toXML()
+ }
+
+ return base + _toXMLString(escapedCharacters, formatting, indentation)
+ }
+
+ private func formatUnsortedXMLElements(
+ _ string: inout String,
+ _ level: Int,
+ _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
+ _ formatting: XMLEncoder.OutputFormatting,
+ _ indentation: XMLEncoder.PrettyPrintIndentation,
+ _ prettyPrinted: Bool
+ ) {
+ formatXMLElements(
+ from: elements,
+ into: &string,
+ at: level,
+ escapedCharacters: escapedCharacters,
+ formatting: formatting,
+ indentation: indentation,
+ prettyPrinted: prettyPrinted
+ )
+ }
+
+ fileprivate func elementString(
+ for element: XMLCoderElement,
+ at level: Int,
+ formatting: XMLEncoder.OutputFormatting,
+ indentation: XMLEncoder.PrettyPrintIndentation,
+ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
+ prettyPrinted: Bool
+ ) -> String {
+ if let stringValue = element.stringValue {
+ if element.isCDATANode {
+ return ""
+ } else {
+ return stringValue.escape(escapedCharacters.elements)
+ }
+ }
+
+ var string = ""
+ let indentLevel = isInlined ? level : level + 1
+ string += element._toXMLString(indented: indentLevel, escapedCharacters, formatting, indentation)
+ string += prettyPrinted && !isInlined ? "\n" : ""
+ return string
+ }
+
+ fileprivate func formatSortedXMLElements(
+ _ string: inout String,
+ _ level: Int,
+ _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
+ _ formatting: XMLEncoder.OutputFormatting,
+ _ indentation: XMLEncoder.PrettyPrintIndentation,
+ _ prettyPrinted: Bool
+ ) {
+ formatXMLElements(from: elements.sorted { $0.key < $1.key },
+ into: &string,
+ at: level,
+ escapedCharacters: escapedCharacters,
+ formatting: formatting,
+ indentation: indentation,
+ prettyPrinted: prettyPrinted)
+ }
+
+ fileprivate func formatXMLAttributes(
+ from attributes: [Attribute],
+ into string: inout String,
+ charactersEscapedInAttributes: [(String, String)]
+ ) {
+ for attribute in attributes {
+ string += " \(attribute.key)=\"\(attribute.value.escape(charactersEscapedInAttributes))\""
+ }
+ }
+
+ fileprivate func formatXMLElements(
+ from elements: [XMLCoderElement],
+ into string: inout String,
+ at level: Int,
+ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
+ formatting: XMLEncoder.OutputFormatting,
+ indentation: XMLEncoder.PrettyPrintIndentation,
+ prettyPrinted: Bool
+ ) {
+ for element in elements {
+ string += elementString(for: element,
+ at: level,
+ formatting: formatting,
+ indentation: indentation,
+ escapedCharacters: escapedCharacters,
+ prettyPrinted: prettyPrinted && !containsTextNodes)
+ }
+ }
+
+ private func formatXMLAttributes(
+ _ formatting: XMLEncoder.OutputFormatting,
+ _ string: inout String,
+ _ charactersEscapedInAttributes: [(String, String)]
+ ) {
+ let attributesBelongingToContainer = self.elements.filter {
+ $0.key.isEmpty && !$0.attributes.isEmpty
+ }.flatMap {
+ $0.attributes
+ }
+ let allAttributes = self.attributes + attributesBelongingToContainer
+
+ let attributes = formatting.contains(.sortedKeys) ?
+ allAttributes.sorted(by: { $0.key < $1.key }) :
+ allAttributes
+ formatXMLAttributes(
+ from: attributes,
+ into: &string,
+ charactersEscapedInAttributes: charactersEscapedInAttributes
+ )
+ }
+
+ private func formatXMLElements(
+ _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
+ _ formatting: XMLEncoder.OutputFormatting,
+ _ indentation: XMLEncoder.PrettyPrintIndentation,
+ _ string: inout String,
+ _ level: Int,
+ _ prettyPrinted: Bool
+ ) {
+ if formatting.contains(.sortedKeys) {
+ formatSortedXMLElements(
+ &string, level, escapedCharacters, formatting, indentation, prettyPrinted
+ )
+ return
+ }
+ formatUnsortedXMLElements(
+ &string, level, escapedCharacters, formatting, indentation, prettyPrinted
+ )
+ }
+
+ private func _toXMLString(
+ indented level: Int = 0,
+ _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
+ _ formatting: XMLEncoder.OutputFormatting,
+ _ indentation: XMLEncoder.PrettyPrintIndentation
+ ) -> String {
+ let prettyPrinted = formatting.contains(.prettyPrinted)
+ let prefix: String
+ switch indentation {
+ case let .spaces(count) where prettyPrinted && !isInlined:
+ prefix = String(repeating: " ", count: level * count)
+ case let .tabs(count) where prettyPrinted && !isInlined:
+ prefix = String(repeating: "\t", count: level * count)
+ default:
+ prefix = ""
+ }
+ var string = prefix
+
+ if !key.isEmpty {
+ string += "<\(key)"
+ formatXMLAttributes(formatting, &string, escapedCharacters.attributes)
+ }
+
+ if !elements.isEmpty || formatting.contains(.noEmptyElements) {
+ let hasOnlyIntrinsicContent = elements.allSatisfy { element in
+ element.key.isEmpty && !element.elements.contains { !$0.key.isEmpty }
+ }
+ let prettyPrintElements = prettyPrinted && !containsTextNodes && !hasOnlyIntrinsicContent
+ if !key.isEmpty {
+ string += prettyPrintElements ? ">\n" : ">"
+ }
+ if !elements.isEmpty {
+ formatXMLElements(escapedCharacters, formatting, indentation, &string, level, prettyPrintElements)
+ }
+
+ if prettyPrintElements { string += prefix }
+ if !key.isEmpty {
+ string += "\(key)>"
+ }
+ } else {
+ if !key.isEmpty {
+ string += " />"
+ }
+ }
+
+ return string
+ }
+}
+
+// MARK: - Convenience Initializers
+
+extension XMLCoderElement {
+ init(key: String, isStringBoxCDATA isCDATA: Bool, box: UnkeyedBox, attributes: [Attribute] = []) {
+ if let containsChoice = box as? [ChoiceBox] {
+ self.init(
+ key: key,
+ elements: containsChoice.map {
+ XMLCoderElement(key: $0.key, isStringBoxCDATA: isCDATA, box: $0.element)
+ },
+ attributes: attributes
+ )
+ } else {
+ self.init(
+ key: key,
+ elements: box.map { XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0) },
+ attributes: attributes
+ )
+ }
+ }
+
+ init(key: String, isStringBoxCDATA: Bool, box: ChoiceBox, attributes: [Attribute] = []) {
+ self.init(
+ key: key,
+ elements: [
+ XMLCoderElement(key: box.key, isStringBoxCDATA: isStringBoxCDATA, box: box.element),
+ ],
+ attributes: attributes
+ )
+ }
+
+ init(key: String, isStringBoxCDATA isCDATA: Bool, box: KeyedBox, attributes: [Attribute] = []) {
+ var elements: [XMLCoderElement] = []
+
+ for (key, box) in box.elements {
+ let fail = {
+ preconditionFailure("Unclassified box: \(type(of: box))")
+ }
+
+ switch box {
+ case let sharedUnkeyedBox as SharedBox:
+ let box = sharedUnkeyedBox.unboxed
+ elements.append(contentsOf: box.map {
+ XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0)
+ })
+ case let unkeyedBox as UnkeyedBox:
+ // This basically injects the unkeyed children directly into self:
+ elements.append(contentsOf: unkeyedBox.map {
+ XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0)
+ })
+ case let sharedKeyedBox as SharedBox:
+ let box = sharedKeyedBox.unboxed
+ elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: box))
+ case let keyedBox as KeyedBox:
+ elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: keyedBox))
+ case let simpleBox as SimpleBox:
+ elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: simpleBox))
+ default:
+ fail()
+ }
+ }
+
+ let attributes: [Attribute] = attributes + box.attributes.compactMap { key, box in
+ guard let value = box.xmlString else {
+ return nil
+ }
+ return Attribute(key: key, value: value)
+ }
+
+ self.init(key: key, elements: elements, attributes: attributes)
+ }
+
+ init(key: String, isStringBoxCDATA: Bool, box: SimpleBox) {
+ if isStringBoxCDATA, let stringBox = box as? StringBox {
+ self.init(key: key, cdataValue: stringBox.unboxed)
+ } else if let value = box.xmlString {
+ self.init(key: key, stringValue: value)
+ } else {
+ self.init(key: key)
+ }
+ }
+
+ init(key: String, isStringBoxCDATA isCDATA: Bool, box: Box, attributes: [Attribute] = []) {
+ switch box {
+ case let sharedUnkeyedBox as SharedBox:
+ self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedUnkeyedBox.unboxed, attributes: attributes)
+ case let sharedKeyedBox as SharedBox:
+ self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedKeyedBox.unboxed, attributes: attributes)
+ case let sharedChoiceBox as SharedBox:
+ self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedChoiceBox.unboxed, attributes: attributes)
+ case let unkeyedBox as UnkeyedBox:
+ self.init(key: key, isStringBoxCDATA: isCDATA, box: unkeyedBox, attributes: attributes)
+ case let keyedBox as KeyedBox:
+ self.init(key: key, isStringBoxCDATA: isCDATA, box: keyedBox, attributes: attributes)
+ case let choiceBox as ChoiceBox:
+ self.init(key: key, isStringBoxCDATA: isCDATA, box: choiceBox, attributes: attributes)
+ case let simpleBox as SimpleBox:
+ self.init(key: key, isStringBoxCDATA: isCDATA, box: simpleBox)
+ case let box:
+ preconditionFailure("Unclassified box: \(type(of: box))")
+ }
+ }
+}
+
+extension XMLCoderElement {
+ func isWhitespaceWithNoElements() -> Bool {
+ let stringValueIsWhitespaceOrNil = stringValue?.isAllWhitespace() ?? true
+ return self.key == "" && stringValueIsWhitespaceOrNil && self.elements.isEmpty
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLDocumentType.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLDocumentType.swift
new file mode 100644
index 0000000000..ec257ebc49
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLDocumentType.swift
@@ -0,0 +1,65 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Joannis Orlandos on 8/11/22.
+//
+
+import Foundation
+
+public struct XMLDocumentType {
+ public enum External: String {
+ case `public` = "PUBLIC"
+ case system = "SYSTEM"
+ }
+
+ public let rootElement: String
+ public let external: External
+ public let dtdName: String?
+ public let dtdLocation: String
+
+ internal init(
+ rootElement: String,
+ external: External,
+ dtdName: String?,
+ dtdLocation: String
+ ) {
+ self.rootElement = rootElement
+ self.external = external
+ self.dtdName = dtdName
+ self.dtdLocation = dtdLocation
+ }
+
+ public static func `public`(rootElement: String, dtdName: String, dtdLocation: String) -> XMLDocumentType {
+ XMLDocumentType(
+ rootElement: rootElement,
+ external: .public,
+ dtdName: dtdName,
+ dtdLocation: dtdLocation
+ )
+ }
+
+ public static func system(rootElement: String, dtdLocation: String) -> XMLDocumentType {
+ XMLDocumentType(
+ rootElement: rootElement,
+ external: .system,
+ dtdName: nil,
+ dtdLocation: dtdLocation
+ )
+ }
+
+ func toXML() -> String {
+ var string = "\n"
+
+ return string
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLHeader.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLHeader.swift
new file mode 100644
index 0000000000..6770a20c76
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLHeader.swift
@@ -0,0 +1,57 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 12/18/18.
+//
+
+import Foundation
+
+/// Type that allows overriding XML header during encoding. Pass a value of this type to the `encode`
+/// function of `XMLEncoder` to specify the exact value of the header you'd like to see in the encoded
+/// data.
+public struct XMLHeader: Sendable {
+ /// The XML standard that the produced document conforms to.
+ public let version: Double?
+
+ /// The encoding standard used to represent the characters in the produced document.
+ public let encoding: String?
+
+ /// Indicates whether a document relies on information from an external source.
+ public let standalone: String?
+
+ public init(version: Double? = nil, encoding: String? = nil, standalone: String? = nil) {
+ self.version = version
+ self.encoding = encoding
+ self.standalone = standalone
+ }
+
+ func isEmpty() -> Bool {
+ return version == nil && encoding == nil && standalone == nil
+ }
+
+ func toXML() -> String? {
+ guard !isEmpty() else {
+ return nil
+ }
+
+ var string = "\n"
+
+ return string
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLKey.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLKey.swift
new file mode 100644
index 0000000000..f7f31f3291
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLKey.swift
@@ -0,0 +1,38 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/21/17.
+//
+
+import Foundation
+
+/// Shared Key Types
+struct XMLKey: CodingKey, Sendable {
+ public let stringValue: String
+ public let intValue: Int?
+
+ public init?(stringValue: String) {
+ self.init(key: stringValue)
+ }
+
+ public init?(intValue: Int) {
+ self.init(index: intValue)
+ }
+
+ public init(stringValue: String, intValue: Int?) {
+ self.stringValue = stringValue
+ self.intValue = intValue
+ }
+
+ init(key: String) {
+ self.init(stringValue: key, intValue: nil)
+ }
+
+ init(index: Int) {
+ self.init(stringValue: "\(index)", intValue: index)
+ }
+
+ static let `super` = XMLKey(stringValue: "super")!
+}
diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLStackParser.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLStackParser.swift
new file mode 100644
index 0000000000..c18a4fd91a
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLStackParser.swift
@@ -0,0 +1,200 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/14/17.
+//
+
+import Foundation
+#if canImport(FoundationXML)
+import FoundationXML
+#endif
+
+class XMLStackParser: NSObject {
+ var root: XMLCoderElement?
+ private var stack: [XMLCoderElement] = []
+ private let trimValueWhitespaces: Bool
+ private let removeWhitespaceElements: Bool
+
+ init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) {
+ self.trimValueWhitespaces = trimValueWhitespaces
+ self.removeWhitespaceElements = removeWhitespaceElements
+ super.init()
+ }
+
+ static func parse(
+ with data: Data,
+ errorContextLength length: UInt,
+ shouldProcessNamespaces: Bool,
+ trimValueWhitespaces: Bool,
+ removeWhitespaceElements: Bool
+ ) throws -> Box {
+ let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces,
+ removeWhitespaceElements: removeWhitespaceElements)
+
+ let node = try parser.parse(
+ with: data,
+ errorContextLength: length,
+ shouldProcessNamespaces: shouldProcessNamespaces
+ )
+
+ return node.transformToBoxTree()
+ }
+
+ func parse(
+ with data: Data,
+ errorContextLength: UInt,
+ shouldProcessNamespaces: Bool
+ ) throws -> XMLCoderElement {
+ let xmlParser = XMLParser(data: data)
+ xmlParser.shouldProcessNamespaces = shouldProcessNamespaces
+ xmlParser.delegate = self
+
+ guard !xmlParser.parse() || root == nil else {
+ return root!
+ }
+
+ guard let error = xmlParser.parserError else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: [],
+ debugDescription: "The given data could not be parsed into XML."
+ ))
+ }
+
+ // `lineNumber` isn't 0-indexed, so 0 is an invalid value for context
+ guard errorContextLength > 0 && xmlParser.lineNumber > 0 else {
+ throw error
+ }
+
+ let string = String(data: data, encoding: .utf8) ?? ""
+ let lines = string.split(separator: "\n")
+ var errorPosition = 0
+ let offset = Int(errorContextLength / 2)
+ for i in 0.. 0 {
+ lowerBoundIndex = errorPosition - offset
+ }
+
+ var upperBoundIndex = string.count
+ if errorPosition + offset < string.count {
+ upperBoundIndex = errorPosition + offset
+ }
+
+ let lowerBound = String.Index(utf16Offset: lowerBoundIndex, in: string)
+ let upperBound = String.Index(utf16Offset: upperBoundIndex, in: string)
+
+ let context = string[lowerBound.. ()) rethrows {
+ guard !stack.isEmpty else {
+ return
+ }
+ try body(&stack[stack.count - 1])
+ }
+
+ func trimWhitespacesIfNeeded(_ string: String) -> String {
+ return trimValueWhitespaces
+ ? string.trimmingCharacters(in: .whitespacesAndNewlines)
+ : string
+ }
+}
+
+extension XMLStackParser: XMLParserDelegate {
+ func parserDidStartDocument(_: XMLParser) {
+ root = nil
+ stack = []
+ }
+
+ func parser(_: XMLParser,
+ didStartElement elementName: String,
+ namespaceURI: String?,
+ qualifiedName: String?,
+ attributes attributeDict: [String: String] = [:])
+ {
+ let attributes = attributeDict.map { key, value in
+ XMLCoderElement.Attribute(key: key, value: value)
+ }
+ let element = XMLCoderElement(key: elementName, attributes: attributes)
+ stack.append(element)
+ }
+
+ func parser(_: XMLParser,
+ didEndElement _: String,
+ namespaceURI _: String?,
+ qualifiedName _: String?)
+ {
+ guard var element = stack.popLast() else {
+ return
+ }
+ if trimValueWhitespaces && element.containsTextNodes {
+ element.trimTextNodes()
+ }
+
+ let updatedElement = removeWhitespaceElements ? elementWithFilteredElements(element: element) : element
+
+ withCurrentElement { currentElement in
+ currentElement.append(element: updatedElement)
+ }
+
+ if stack.isEmpty {
+ root = updatedElement
+ }
+ }
+
+ func elementWithFilteredElements(element: XMLCoderElement) -> XMLCoderElement {
+ var hasWhitespaceElements = false
+ var hasNonWhitespaceElements = false
+ var filteredElements: [XMLCoderElement] = []
+ for ele in element.elements {
+ if ele.isWhitespaceWithNoElements() {
+ hasWhitespaceElements = true
+ } else {
+ hasNonWhitespaceElements = true
+ filteredElements.append(ele)
+ }
+ }
+
+ if hasWhitespaceElements && hasNonWhitespaceElements {
+ return XMLCoderElement(key: element.key, elements: filteredElements, attributes: element.attributes)
+ }
+ return element
+ }
+
+ func parser(_: XMLParser, foundCharacters string: String) {
+ let processedString = trimWhitespacesIfNeeded(string)
+ guard processedString.count > 0, string.count != 0 else {
+ return
+ }
+
+ withCurrentElement { currentElement in
+ currentElement.append(string: string)
+ }
+ }
+
+ func parser(_: XMLParser, foundCDATA CDATABlock: Data) {
+ guard let string = String(data: CDATABlock, encoding: .utf8) else {
+ return
+ }
+
+ withCurrentElement { currentElement in
+ currentElement.append(cdata: string)
+ }
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Decoder/DecodingErrorExtension.swift b/third-party/XMLCoder/Sources/Decoder/DecodingErrorExtension.swift
new file mode 100644
index 0000000000..011477ec2c
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Decoder/DecodingErrorExtension.swift
@@ -0,0 +1,54 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/21/17.
+//
+
+import Foundation
+
+// MARK: - Error Utilities
+
+extension DecodingError {
+ /// Returns a `.typeMismatch` error describing the expected type.
+ ///
+ /// - parameter path: The path of `CodingKey`s taken to decode a value of this type.
+ /// - parameter expectation: The type expected to be encountered.
+ /// - parameter reality: The value that was encountered instead of the expected type.
+ /// - returns: A `DecodingError` with the appropriate path and debug description.
+ static func typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Box) -> DecodingError {
+ let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead."
+ return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description))
+ }
+
+ /// Returns a description of the type of `value` appropriate for an error message.
+ ///
+ /// - parameter value: The value whose type to describe.
+ /// - returns: A string describing `value`.
+ /// - precondition: `value` is one of the types below.
+ static func _typeDescription(of box: Box) -> String {
+ switch box {
+ case is NullBox:
+ return "a null value"
+ case is BoolBox:
+ return "a boolean value"
+ case is DecimalBox:
+ return "a decimal value"
+ case is IntBox:
+ return "a signed integer value"
+ case is UIntBox:
+ return "an unsigned integer value"
+ case is FloatBox:
+ return "a floating-point value"
+ case is DoubleBox:
+ return "a double floating-point value"
+ case is UnkeyedBox:
+ return "a array value"
+ case is KeyedBox:
+ return "a dictionary value"
+ case _:
+ return "\(type(of: box))"
+ }
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Decoder/DynamicNodeDecoding.swift b/third-party/XMLCoder/Sources/Decoder/DynamicNodeDecoding.swift
new file mode 100644
index 0000000000..9c87ba4326
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Decoder/DynamicNodeDecoding.swift
@@ -0,0 +1,44 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Max Desiatov on 01/03/2019.
+//
+
+/** Allows conforming types to specify how its properties will be decoded.
+
+ For example:
+ ```swift
+ struct Book: Codable, Equatable, DynamicNodeDecoding {
+ let id: UInt
+ let title: String
+ let categories: [Category]
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case title
+ case categories = "category"
+ }
+
+ static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
+ switch key {
+ case Book.CodingKeys.id: return .attribute
+ default: return .element
+ }
+ }
+ }
+ ```
+ allows XML of this form to be decoded into values of type `Book`:
+
+ ```xml
+
+ Cat in the Hat
+ Kids
+ Wildlife
+
+ ```
+ */
+public protocol DynamicNodeDecoding: Decodable {
+ static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
+}
diff --git a/third-party/XMLCoder/Sources/Decoder/SingleValueDecodingContainer.swift b/third-party/XMLCoder/Sources/Decoder/SingleValueDecodingContainer.swift
new file mode 100644
index 0000000000..0f08b22082
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Decoder/SingleValueDecodingContainer.swift
@@ -0,0 +1,57 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/20/17.
+//
+
+import Foundation
+
+extension XMLDecoderImplementation: SingleValueDecodingContainer {
+ // MARK: SingleValueDecodingContainer Methods
+
+ public func decodeNil() -> Bool {
+ return (try? topContainer().isNull) ?? true
+ }
+
+ public func decode(_: Bool.Type) throws -> Bool {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: Decimal.Type) throws -> Decimal {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: T.Type) throws -> T {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: T.Type) throws -> T {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: Float.Type) throws -> Float {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: Double.Type) throws -> Double {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: String.Type) throws -> String {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: String.Type) throws -> Date {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: String.Type) throws -> Data {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: T.Type) throws -> T {
+ return try unbox(try topContainer())
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Decoder/XMLChoiceDecodingContainer.swift b/third-party/XMLCoder/Sources/Decoder/XMLChoiceDecodingContainer.swift
new file mode 100644
index 0000000000..7d4c1026b5
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Decoder/XMLChoiceDecodingContainer.swift
@@ -0,0 +1,105 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by James Bean on 7/18/19.
+//
+
+/// Container specialized for decoding XML choice elements.
+struct XMLChoiceDecodingContainer: KeyedDecodingContainerProtocol {
+ typealias Key = K
+
+ // MARK: Properties
+
+ /// A reference to the decoder we're reading from.
+ private let decoder: XMLDecoderImplementation
+
+ /// A reference to the container we're reading from.
+ private let container: SharedBox
+
+ /// The path of coding keys taken to get to this point in decoding.
+ public private(set) var codingPath: [CodingKey]
+
+ // MARK: - Initialization
+
+ /// Initializes `self` by referencing the given decoder and container.
+ init(referencing decoder: XMLDecoderImplementation, wrapping container: SharedBox) {
+ self.decoder = decoder
+ container.withShared { $0.key = decoder.keyTransform($0.key) }
+ self.container = container
+ codingPath = decoder.codingPath
+ }
+
+ // MARK: - KeyedDecodingContainerProtocol Methods
+
+ public var allKeys: [Key] {
+ return container.withShared { [Key(stringValue: $0.key)!] }
+ }
+
+ public func contains(_ key: Key) -> Bool {
+ return container.withShared { $0.key == key.stringValue }
+ }
+
+ public func decodeNil(forKey key: Key) throws -> Bool {
+ return container.withShared { $0.element.isNull }
+ }
+
+ public func decode(_ type: T.Type, forKey key: Key) throws -> T {
+ guard container.withShared({ $0.key == key.stringValue }), key is XMLChoiceCodingKey else {
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: type,
+ reality: container
+ )
+ }
+ return try decoder.unbox(container.withShared { $0.element })
+ }
+
+ public func nestedContainer(
+ keyedBy _: NestedKey.Type, forKey key: Key
+ ) throws -> KeyedDecodingContainer {
+ guard container.unboxed.key == key.stringValue else {
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: NestedKey.self,
+ reality: container
+ )
+ }
+
+ let value = container.unboxed.element
+ guard let container = XMLKeyedDecodingContainer(box: value, decoder: decoder) else {
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: [String: Any].self,
+ reality: value
+ )
+ }
+
+ return KeyedDecodingContainer(container)
+ }
+
+ public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: Key.self,
+ reality: container
+ )
+ }
+
+ public func superDecoder() throws -> Decoder {
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: Key.self,
+ reality: container
+ )
+ }
+
+ public func superDecoder(forKey key: Key) throws -> Decoder {
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: Key.self,
+ reality: container
+ )
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Decoder/XMLDecoder.swift b/third-party/XMLCoder/Sources/Decoder/XMLDecoder.swift
new file mode 100644
index 0000000000..1f67b26975
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Decoder/XMLDecoder.swift
@@ -0,0 +1,405 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/20/17.
+//
+
+import Foundation
+
+// MARK: - XML Decoder
+
+/// `XMLDecoder` facilitates the decoding of XML into semantic `Decodable` types.
+open class XMLDecoder {
+ // MARK: Options
+
+ /// The strategy to use for decoding `Date` values.
+ public enum DateDecodingStrategy {
+ /// Defer to `Date` for decoding. This is the default strategy.
+ case deferredToDate
+
+ /// Decode the `Date` as a UNIX timestamp from a XML number. This is the default strategy.
+ case secondsSince1970
+
+ /// Decode the `Date` as UNIX millisecond timestamp from a XML number.
+ case millisecondsSince1970
+
+ /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
+ @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
+ case iso8601
+
+ /// Decode the `Date` as a string parsed by the given formatter.
+ case formatted(DateFormatter)
+
+ /// Decode the `Date` as a custom box decoded by the given closure.
+ case custom((_ decoder: Decoder) throws -> Date)
+
+ /// Decode the `Date` as a string parsed by the given formatter for the give key.
+ static func keyFormatted(
+ _ formatterForKey: @escaping (CodingKey) throws -> DateFormatter?
+ ) -> XMLDecoder.DateDecodingStrategy {
+ return .custom { decoder -> Date in
+ guard let codingKey = decoder.codingPath.last else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "No Coding Path Found"
+ ))
+ }
+
+ guard let container = try? decoder.singleValueContainer(),
+ let text = try? container.decode(String.self)
+ else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Could not decode date text"
+ ))
+ }
+
+ guard let dateFormatter = try formatterForKey(codingKey) else {
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "No date formatter for date text"
+ )
+ }
+
+ if let date = dateFormatter.date(from: text) {
+ return date
+ } else {
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "Cannot decode date string \(text)"
+ )
+ }
+ }
+ }
+ }
+
+ /// The strategy to use for decoding `Data` values.
+ public enum DataDecodingStrategy {
+ /// Defer to `Data` for decoding.
+ case deferredToData
+
+ /// Decode the `Data` from a Base64-encoded string. This is the default strategy.
+ case base64
+
+ /// Decode the `Data` as a custom box decoded by the given closure.
+ case custom((_ decoder: Decoder) throws -> Data)
+
+ /// Decode the `Data` as a custom box by the given closure for the give key.
+ static func keyFormatted(
+ _ formatterForKey: @escaping (CodingKey) throws -> Data?
+ ) -> XMLDecoder.DataDecodingStrategy {
+ return .custom { decoder -> Data in
+ guard let codingKey = decoder.codingPath.last else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "No Coding Path Found"
+ ))
+ }
+
+ guard let container = try? decoder.singleValueContainer(),
+ let text = try? container.decode(String.self)
+ else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Could not decode date text"
+ ))
+ }
+
+ guard let data = try formatterForKey(codingKey) else {
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "Cannot decode data string \(text)"
+ )
+ }
+
+ return data
+ }
+ }
+ }
+
+ /// The strategy to use for non-XML-conforming floating-point values (IEEE 754 infinity and NaN).
+ public enum NonConformingFloatDecodingStrategy {
+ /// Throw upon encountering non-conforming values. This is the default strategy.
+ case `throw`
+
+ /// Decode the values from the given representation strings.
+ case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
+ }
+
+ /// The strategy to use for automatically changing the box of keys before decoding.
+ public enum KeyDecodingStrategy {
+ /// Use the keys specified by each type. This is the default strategy.
+ case useDefaultKeys
+
+ /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting
+ /// to match a key with the one specified by each type.
+ ///
+ /// The conversion to upper case uses `Locale.system`, also known as
+ /// the ICU "root" locale. This means the result is consistent
+ /// regardless of the current user's locale and language preferences.
+ ///
+ /// Converting from snake case to camel case:
+ /// 1. Capitalizes the word starting after each `_`
+ /// 2. Removes all `_`
+ /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).
+ /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.
+ ///
+ /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character.
+ case convertFromSnakeCase
+
+ /// Convert from "kebab-case" to "kebabCase" before attempting
+ /// to match a key with the one specified by each type.
+ case convertFromKebabCase
+
+ /// Convert from "CodingKey" to "codingKey"
+ case convertFromCapitalized
+
+ /// Convert from "CODING_KEY" to "codingKey"
+ case convertFromUppercase
+
+ /// Provide a custom conversion from the key in the encoded XML to the
+ /// keys specified by the decoded types.
+ /// The full path to the current decoding position is provided for
+ /// context (in case you need to locate this key within the payload).
+ /// The returned key is used in place of the last component in the
+ /// coding path before decoding.
+ /// If the result of the conversion is a duplicate key, then only one
+ /// box will be present in the container for the type to decode from.
+ case custom((_ codingPath: [CodingKey]) -> CodingKey)
+
+ static func _convertFromCapitalized(_ stringKey: String) -> String {
+ guard !stringKey.isEmpty else {
+ return stringKey
+ }
+ let firstLetter = stringKey.prefix(1).lowercased()
+ let result = firstLetter + stringKey.dropFirst()
+ return result
+ }
+
+ static func _convertFromUppercase(_ stringKey: String) -> String {
+ _convert(stringKey.lowercased(), usingSeparator: "_")
+ }
+
+ static func _convertFromSnakeCase(_ stringKey: String) -> String {
+ return _convert(stringKey, usingSeparator: "_")
+ }
+
+ static func _convertFromKebabCase(_ stringKey: String) -> String {
+ return _convert(stringKey, usingSeparator: "-")
+ }
+
+ static func _convert(_ stringKey: String, usingSeparator separator: Character) -> String {
+ guard !stringKey.isEmpty else {
+ return stringKey
+ }
+
+ // Find the first non-separator character
+ guard let firstNonSeparator = stringKey.firstIndex(where: { $0 != separator }) else {
+ // Reached the end without finding a separator character
+ return stringKey
+ }
+
+ // Find the last non-separator character
+ var lastNonSeparator = stringKey.index(before: stringKey.endIndex)
+ while lastNonSeparator > firstNonSeparator, stringKey[lastNonSeparator] == separator {
+ stringKey.formIndex(before: &lastNonSeparator)
+ }
+
+ let keyRange = firstNonSeparator...lastNonSeparator
+ let leadingSeparatorRange = stringKey.startIndex..value`.
+ case element
+ /// Decodes a node from either elements of form `value` or attributes
+ /// of form `nodeName="value"`, with elements taking priority.
+ case elementOrAttribute
+ }
+
+ /// The strategy to use in encoding encoding attributes. Defaults to `.deferredToEncoder`.
+ open var nodeDecodingStrategy: NodeDecodingStrategy = .deferredToDecoder
+
+ /// Set of strategies to use for encoding of nodes.
+ public enum NodeDecodingStrategy {
+ /// Defer to `Encoder` for choosing an encoding. This is the default strategy.
+ case deferredToDecoder
+
+ /// Return a closure computing the desired node encoding for the value by its coding key.
+ case custom((Decodable.Type, Decoder) -> ((CodingKey) -> NodeDecoding))
+
+ func nodeDecodings(
+ forType codableType: Decodable.Type,
+ with decoder: Decoder
+ ) -> ((CodingKey) -> NodeDecoding?) {
+ switch self {
+ case .deferredToDecoder:
+ guard let dynamicType = codableType as? DynamicNodeDecoding.Type else {
+ return { _ in nil }
+ }
+ return dynamicType.nodeDecoding(for:)
+ case let .custom(closure):
+ return closure(codableType, decoder)
+ }
+ }
+ }
+
+ /// Contextual user-provided information for use during decoding.
+ open var userInfo: [CodingUserInfoKey: Any] = [:]
+
+ /// The error context length. Non-zero length makes an error thrown from
+ /// the XML parser with line/column location repackaged with a context
+ /// around that location of specified length. For example, if an error was
+ /// thrown indicating that there's an unexpected character at line 3, column
+ /// 15 with `errorContextLength` set to 10, a new error type is rethrown
+ /// containing 5 characters before column 15 and 5 characters after, all on
+ /// line 3. Line wrapping should be handled correctly too as the context can
+ /// span more than a few lines.
+ open var errorContextLength: UInt = 0
+
+ /** A boolean value that determines whether the parser reports the
+ namespaces and qualified names of elements. The default value is `false`.
+ */
+ open var shouldProcessNamespaces: Bool = false
+
+ /** A boolean value that determines whether the parser trims whitespaces
+ and newlines from the end and the beginning of string values. The default
+ value is `true`.
+ */
+ open var trimValueWhitespaces: Bool
+
+ /** A boolean value that determines whether to remove pure whitespace elements
+ that have sibling elements that aren't pure whitespace. The default value
+ is `false`.
+ */
+ open var removeWhitespaceElements: Bool
+
+ /// Options set on the top-level encoder to pass down the decoding hierarchy.
+ struct Options {
+ let dateDecodingStrategy: DateDecodingStrategy
+ let dataDecodingStrategy: DataDecodingStrategy
+ let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
+ let keyDecodingStrategy: KeyDecodingStrategy
+ let nodeDecodingStrategy: NodeDecodingStrategy
+ let userInfo: [CodingUserInfoKey: Any]
+ }
+
+ /// The options set on the top-level decoder.
+ var options: Options {
+ return Options(
+ dateDecodingStrategy: dateDecodingStrategy,
+ dataDecodingStrategy: dataDecodingStrategy,
+ nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
+ keyDecodingStrategy: keyDecodingStrategy,
+ nodeDecodingStrategy: nodeDecodingStrategy,
+ userInfo: userInfo
+ )
+ }
+
+ // MARK: - Constructing a XML Decoder
+
+ /// Initializes `self` with default strategies.
+ public init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) {
+ self.trimValueWhitespaces = trimValueWhitespaces
+ self.removeWhitespaceElements = removeWhitespaceElements
+ }
+
+ // MARK: - Decoding Values
+
+ /// Decodes a top-level box of the given type from the given XML representation.
+ ///
+ /// - parameter type: The type of the box to decode.
+ /// - parameter data: The data to decode from.
+ /// - returns: A box of the requested type.
+ /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid XML.
+ /// - throws: An error if any box throws an error during decoding.
+ open func decode(
+ _ type: T.Type,
+ from data: Data
+ ) throws -> T {
+ let topLevel: Box = try XMLStackParser.parse(
+ with: data,
+ errorContextLength: errorContextLength,
+ shouldProcessNamespaces: shouldProcessNamespaces,
+ trimValueWhitespaces: trimValueWhitespaces,
+ removeWhitespaceElements: removeWhitespaceElements
+ )
+
+ let decoder = XMLDecoderImplementation(
+ referencing: topLevel,
+ options: options,
+ nodeDecodings: []
+ )
+ decoder.nodeDecodings = [
+ options.nodeDecodingStrategy.nodeDecodings(
+ forType: T.self,
+ with: decoder
+ ),
+ ]
+
+ defer {
+ _ = decoder.nodeDecodings.removeLast()
+ }
+
+ return try decoder.unbox(topLevel)
+ }
+}
+
+// MARK: TopLevelDecoder
+
+#if canImport(Combine)
+import protocol Combine.TopLevelDecoder
+import protocol Combine.TopLevelEncoder
+#elseif canImport(OpenCombine)
+import protocol OpenCombine.TopLevelDecoder
+import protocol OpenCombine.TopLevelEncoder
+#endif
+
+#if canImport(Combine) || canImport(OpenCombine)
+extension XMLDecoder: TopLevelDecoder {}
+extension XMLEncoder: TopLevelEncoder {}
+#endif
diff --git a/third-party/XMLCoder/Sources/Decoder/XMLDecoderImplementation.swift b/third-party/XMLCoder/Sources/Decoder/XMLDecoderImplementation.swift
new file mode 100644
index 0000000000..961322f35d
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Decoder/XMLDecoderImplementation.swift
@@ -0,0 +1,497 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/20/17.
+//
+
+import Foundation
+
+class XMLDecoderImplementation: Decoder {
+ // MARK: Properties
+
+ /// The decoder's storage.
+ var storage = XMLDecodingStorage()
+
+ /// Options set on the top-level decoder.
+ let options: XMLDecoder.Options
+
+ /// The path to the current point in encoding.
+ public internal(set) var codingPath: [CodingKey]
+
+ public var nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding?]
+
+ /// Contextual user-provided information for use during encoding.
+ public var userInfo: [CodingUserInfoKey: Any] {
+ return options.userInfo
+ }
+
+ // The error context length
+ open var errorContextLength: UInt = 0
+
+ // MARK: - Initialization
+
+ /// Initializes `self` with the given top-level container and options.
+ init(
+ referencing container: Box,
+ options: XMLDecoder.Options,
+ nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding?],
+ codingPath: [CodingKey] = []
+ ) {
+ storage.push(container: container)
+ self.codingPath = codingPath
+ self.nodeDecodings = nodeDecodings
+ self.options = options
+ }
+
+ // MARK: - Decoder Methods
+
+ internal func topContainer() throws -> Box {
+ guard let topContainer = storage.topContainer() else {
+ throw DecodingError.valueNotFound(Box.self, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Cannot get decoding container -- empty container stack."
+ ))
+ }
+ return topContainer
+ }
+
+ private func popContainer() throws -> Box {
+ guard let topContainer = storage.popContainer() else {
+ throw DecodingError.valueNotFound(Box.self, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription:
+ """
+ Cannot get decoding container -- empty container stack.
+ """
+ ))
+ }
+ return topContainer
+ }
+
+ public func container(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer {
+ if let keyed = try topContainer() as? SharedBox {
+ return KeyedDecodingContainer(XMLKeyedDecodingContainer(
+ referencing: self,
+ wrapping: keyed
+ ))
+ }
+ if Key.self is XMLChoiceCodingKey.Type {
+ return try choiceContainer(keyedBy: keyType)
+ } else {
+ return try keyedContainer(keyedBy: keyType)
+ }
+ }
+
+ public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
+ let topContainer = try self.topContainer()
+
+ guard !topContainer.isNull else {
+ throw DecodingError.valueNotFound(
+ UnkeyedDecodingContainer.self,
+ DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription:
+ """
+ Cannot get unkeyed decoding container -- found null box instead.
+ """
+ )
+ )
+ }
+
+ switch topContainer {
+ case let unkeyed as SharedBox:
+ return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
+ case let keyed as SharedBox:
+ return XMLUnkeyedDecodingContainer(
+ referencing: self,
+ wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleKeyedBox.init) })
+ )
+ default:
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: [Any].self,
+ reality: topContainer
+ )
+ }
+ }
+
+ public func singleValueContainer() throws -> SingleValueDecodingContainer {
+ return self
+ }
+
+ private func keyedContainer(keyedBy _: Key.Type) throws -> KeyedDecodingContainer {
+ let topContainer = try self.topContainer()
+ let keyedBox: KeyedBox
+ switch topContainer {
+ case _ where topContainer.isNull:
+ throw DecodingError.valueNotFound(
+ KeyedDecodingContainer.self,
+ DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription:
+ """
+ Cannot get keyed decoding container -- found null box instead.
+ """
+ )
+ )
+ case let string as StringBox:
+ keyedBox = KeyedBox(
+ elements: KeyedStorage([("", string)]),
+ attributes: KeyedStorage()
+ )
+ case let containsEmpty as SingleKeyedBox where containsEmpty.element is NullBox:
+ keyedBox = KeyedBox(
+ elements: KeyedStorage([("", StringBox(""))]),
+ attributes: KeyedStorage()
+ )
+ case let unkeyed as SharedBox:
+ guard let keyed = unkeyed.withShared({ $0.first }) as? KeyedBox else {
+ fallthrough
+ }
+ keyedBox = keyed
+ default:
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: [String: Any].self,
+ reality: topContainer
+ )
+ }
+ let container = XMLKeyedDecodingContainer(
+ referencing: self,
+ wrapping: SharedBox(keyedBox)
+ )
+ return KeyedDecodingContainer(container)
+ }
+
+ /// - Returns: A `KeyedDecodingContainer` for an XML choice element.
+ private func choiceContainer(keyedBy _: Key.Type) throws -> KeyedDecodingContainer {
+ let topContainer = try self.topContainer()
+ let choiceBox: ChoiceBox
+ switch topContainer {
+ case let choice as ChoiceBox:
+ choiceBox = choice
+ case let singleKeyed as SingleKeyedBox:
+ choiceBox = ChoiceBox(singleKeyed)
+ default:
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: [String: Any].self,
+ reality: topContainer
+ )
+ }
+ let container = XMLChoiceDecodingContainer(
+ referencing: self,
+ wrapping: SharedBox(choiceBox)
+ )
+ return KeyedDecodingContainer(container)
+ }
+}
+
+// MARK: - Concrete Value Representations
+
+extension XMLDecoderImplementation {
+ /// Returns the given box unboxed from a container.
+ private func typedBox(_ box: Box, for valueType: T.Type) throws -> B {
+ let error = DecodingError.valueNotFound(valueType, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Expected \(valueType) but found null instead."
+ ))
+ switch box {
+ case let typedBox as B:
+ return typedBox
+ case let unkeyedBox as SharedBox:
+ guard let value = unkeyedBox.withShared({
+ $0.first as? B
+ }) else { throw error }
+ return value
+ case let keyedBox as SharedBox:
+ guard
+ let value = keyedBox.withShared({ $0.value as? B })
+ else { throw error }
+ return value
+ case let singleKeyedBox as SingleKeyedBox:
+ if let value = singleKeyedBox.element as? B {
+ return value
+ } else if let box = singleKeyedBox.element as? KeyedBox, let value = box.elements[""].first as? B {
+ return value
+ } else {
+ throw error
+ }
+ case is NullBox:
+ throw error
+ case let keyedBox as KeyedBox:
+ guard
+ let value = keyedBox.value as? B
+ else { fallthrough }
+ return value
+ default:
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: valueType,
+ reality: box
+ )
+ }
+ }
+
+ func unbox(_ box: Box) throws -> Bool {
+ let stringBox: StringBox = try typedBox(box, for: Bool.self)
+ let string = stringBox.unboxed
+
+ guard let boolBox = BoolBox(xmlString: string) else {
+ throw DecodingError.typeMismatch(at: codingPath, expectation: Bool.self, reality: box)
+ }
+
+ return boolBox.unboxed
+ }
+
+ func unbox(_ box: Box) throws -> Decimal {
+ let stringBox: StringBox = try typedBox(box, for: Decimal.self)
+ let string = stringBox.unboxed
+
+ guard let decimalBox = DecimalBox(xmlString: string) else {
+ throw DecodingError.typeMismatch(at: codingPath, expectation: Decimal.self, reality: box)
+ }
+
+ return decimalBox.unboxed
+ }
+
+ func unbox(_ box: Box) throws -> T {
+ let stringBox: StringBox = try typedBox(box, for: T.self)
+ let string = stringBox.unboxed
+
+ guard let intBox = IntBox(xmlString: string) else {
+ throw DecodingError.typeMismatch(at: codingPath, expectation: T.self, reality: box)
+ }
+
+ guard let int: T = intBox.unbox() else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)."
+ ))
+ }
+
+ return int
+ }
+
+ func unbox(_ box: Box) throws -> T {
+ let stringBox: StringBox = try typedBox(box, for: T.self)
+ let string = stringBox.unboxed
+
+ guard let uintBox = UIntBox(xmlString: string) else {
+ throw DecodingError.typeMismatch(at: codingPath, expectation: T.self, reality: box)
+ }
+
+ guard let uint: T = uintBox.unbox() else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)."
+ ))
+ }
+
+ return uint
+ }
+
+ func unbox(_ box: Box) throws -> Float {
+ let stringBox: StringBox = try typedBox(box, for: Float.self)
+ let string = stringBox.unboxed
+
+ guard let floatBox = FloatBox(xmlString: string) else {
+ throw DecodingError.typeMismatch(at: codingPath, expectation: Float.self, reality: box)
+ }
+
+ return floatBox.unboxed
+ }
+
+ func unbox(_ box: Box) throws -> Double {
+ let stringBox: StringBox = try typedBox(box, for: Double.self)
+ let string = stringBox.unboxed
+
+ guard let doubleBox = DoubleBox(xmlString: string) else {
+ throw DecodingError.typeMismatch(at: codingPath, expectation: Double.self, reality: box)
+ }
+
+ return doubleBox.unboxed
+ }
+
+ func unbox(_ box: Box) throws -> String {
+ do {
+ let stringBox: StringBox = try typedBox(box, for: String.self)
+ return stringBox.unboxed
+ } catch {
+ if box is NullBox {
+ return ""
+ }
+ }
+
+ return ""
+ }
+
+ func unbox(_ box: Box) throws -> Date {
+ switch options.dateDecodingStrategy {
+ case .deferredToDate:
+ storage.push(container: box)
+ defer { storage.popContainer() }
+ return try Date(from: self)
+
+ case .secondsSince1970:
+ let stringBox: StringBox = try typedBox(box, for: Date.self)
+ let string = stringBox.unboxed
+
+ guard let dateBox = DateBox(secondsSince1970: string) else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Expected date string to be formatted in seconds since 1970."
+ ))
+ }
+ return dateBox.unboxed
+ case .millisecondsSince1970:
+ let stringBox: StringBox = try typedBox(box, for: Date.self)
+ let string = stringBox.unboxed
+
+ guard let dateBox = DateBox(millisecondsSince1970: string) else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Expected date string to be formatted in milliseconds since 1970."
+ ))
+ }
+ return dateBox.unboxed
+ case .iso8601:
+ let stringBox: StringBox = try typedBox(box, for: Date.self)
+ let string = stringBox.unboxed
+
+ guard let dateBox = DateBox(iso8601: string) else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Expected date string to be ISO8601-formatted."
+ ))
+ }
+ return dateBox.unboxed
+ case let .formatted(formatter):
+ let stringBox: StringBox = try typedBox(box, for: Date.self)
+ let string = stringBox.unboxed
+
+ guard let dateBox = DateBox(xmlString: string, formatter: formatter) else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Date string does not match format expected by formatter."
+ ))
+ }
+ return dateBox.unboxed
+ case let .custom(closure):
+ storage.push(container: box)
+ defer { storage.popContainer() }
+ return try closure(self)
+ }
+ }
+
+ func unbox(_ box: Box) throws -> Data {
+ switch options.dataDecodingStrategy {
+ case .deferredToData:
+ storage.push(container: box)
+ defer { storage.popContainer() }
+ return try Data(from: self)
+ case .base64:
+ let stringBox: StringBox = try typedBox(box, for: Data.self)
+ let string = stringBox.unboxed
+
+ guard let dataBox = DataBox(base64: string) else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Encountered Data is not valid Base64"
+ ))
+ }
+ return dataBox.unboxed
+ case let .custom(closure):
+ storage.push(container: box)
+ defer { storage.popContainer() }
+ return try closure(self)
+ }
+ }
+
+ func unbox(_ box: Box) throws -> URL {
+ let stringBox: StringBox = try typedBox(box, for: URL.self)
+ let string = stringBox.unboxed
+
+ guard let urlBox = URLBox(xmlString: string) else {
+ throw DecodingError.dataCorrupted(DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Encountered Data is not valid Base64"
+ ))
+ }
+
+ return urlBox.unboxed
+ }
+
+ func unbox(_ box: Box) throws -> T {
+ let decoded: T?
+ let type = T.self
+
+ if type == Date.self || type == NSDate.self {
+ let date: Date = try unbox(box)
+ decoded = date as? T
+ } else if type == Data.self || type == NSData.self {
+ let data: Data = try unbox(box)
+ decoded = data as? T
+ } else if type == URL.self || type == NSURL.self {
+ let data: URL = try unbox(box)
+ decoded = data as? T
+ } else if type == Decimal.self || type == NSDecimalNumber.self {
+ let decimal: Decimal = try unbox(box)
+ decoded = decimal as? T
+ } else if
+ type == String.self || type == NSString.self,
+ let value = (try unbox(box) as String) as? T
+ {
+ decoded = value
+ } else {
+ storage.push(container: box)
+ defer {
+ storage.popContainer()
+ }
+
+ do {
+ decoded = try type.init(from: self)
+ } catch {
+ guard case DecodingError.valueNotFound = error,
+ let type = type as? AnyOptional.Type,
+ let result = type.init() as? T
+ else {
+ throw error
+ }
+
+ return result
+ }
+ }
+
+ guard let result = decoded else {
+ throw DecodingError.typeMismatch(
+ at: codingPath, expectation: type, reality: box
+ )
+ }
+
+ return result
+ }
+}
+
+extension XMLDecoderImplementation {
+ var keyTransform: (String) -> String {
+ switch options.keyDecodingStrategy {
+ case .convertFromSnakeCase:
+ return XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase
+ case .convertFromCapitalized:
+ return XMLDecoder.KeyDecodingStrategy._convertFromCapitalized
+ case .convertFromUppercase:
+ return XMLDecoder.KeyDecodingStrategy._convertFromUppercase
+ case .convertFromKebabCase:
+ return XMLDecoder.KeyDecodingStrategy._convertFromKebabCase
+ case .useDefaultKeys:
+ return { key in key }
+ case let .custom(converter):
+ return { key in
+ converter(self.codingPath + [XMLKey(stringValue: key, intValue: nil)]).stringValue
+ }
+ }
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Decoder/XMLDecodingStorage.swift b/third-party/XMLCoder/Sources/Decoder/XMLDecodingStorage.swift
new file mode 100644
index 0000000000..18d3bdd0ab
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Decoder/XMLDecodingStorage.swift
@@ -0,0 +1,52 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/20/17.
+//
+
+import Foundation
+
+// MARK: - Decoding Storage
+
+struct XMLDecodingStorage {
+ // MARK: Properties
+
+ /// The container stack.
+ /// Elements may be any one of the XML types (StringBox, KeyedBox).
+ private var containers: [Box] = []
+
+ // MARK: - Initialization
+
+ /// Initializes `self` with no containers.
+ init() {}
+
+ // MARK: - Modifying the Stack
+
+ var count: Int {
+ return containers.count
+ }
+
+ func topContainer() -> Box? {
+ return containers.last
+ }
+
+ mutating func push(container: Box) {
+ if let keyedBox = container as? KeyedBox {
+ containers.append(SharedBox(keyedBox))
+ } else if let unkeyedBox = container as? UnkeyedBox {
+ containers.append(SharedBox(unkeyedBox))
+ } else {
+ containers.append(container)
+ }
+ }
+
+ @discardableResult
+ mutating func popContainer() -> Box? {
+ guard !containers.isEmpty else {
+ return nil
+ }
+ return containers.removeLast()
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Decoder/XMLKeyedDecodingContainer.swift b/third-party/XMLCoder/Sources/Decoder/XMLKeyedDecodingContainer.swift
new file mode 100644
index 0000000000..826b92b90a
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Decoder/XMLKeyedDecodingContainer.swift
@@ -0,0 +1,417 @@
+// Copyright (c) 2017-2021 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/21/17.
+//
+
+import Foundation
+
+// MARK: Decoding Containers
+
+struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol {
+ typealias Key = K
+ typealias KeyedContainer = SharedBox
+ typealias UnkeyedContainer = SharedBox
+
+ // MARK: Properties
+
+ /// A reference to the decoder we're reading from.
+ private let decoder: XMLDecoderImplementation
+
+ /// A reference to the container we're reading from.
+ private let container: KeyedContainer
+
+ /// The path of coding keys taken to get to this point in decoding.
+ public private(set) var codingPath: [CodingKey]
+
+ // MARK: - Initialization
+
+ /// Initializes `self` by referencing the given decoder and container.
+ init(
+ referencing decoder: XMLDecoderImplementation,
+ wrapping container: KeyedContainer
+ ) {
+ self.decoder = decoder
+ container.withShared {
+ $0.elements = .init($0.elements.map { (decoder.keyTransform($0), $1) })
+ $0.attributes = .init($0.attributes.map { (decoder.keyTransform($0), $1) })
+ }
+ self.container = container
+ codingPath = decoder.codingPath
+ }
+
+ // MARK: - KeyedDecodingContainerProtocol Methods
+
+ public var allKeys: [Key] {
+ let elementKeys = container.withShared { keyedBox in
+ keyedBox.elements.keys.compactMap { Key(stringValue: $0) }
+ }
+
+ let attributeKeys = container.withShared { keyedBox in
+ keyedBox.attributes.keys.compactMap { Key(stringValue: $0) }
+ }
+
+ return attributeKeys + elementKeys
+ }
+
+ public func contains(_ key: Key) -> Bool {
+ let elements = container.withShared { keyedBox in
+ keyedBox.elements[key.stringValue]
+ }
+
+ let attributes = container.withShared { keyedBox in
+ keyedBox.attributes[key.stringValue]
+ }
+
+ return !elements.isEmpty || !attributes.isEmpty
+ }
+
+ public func decodeNil(forKey key: Key) throws -> Bool {
+ let elements = container.withShared { keyedBox in
+ keyedBox.elements[key.stringValue]
+ }
+
+ let attributes = container.withShared { keyedBox in
+ keyedBox.attributes[key.stringValue]
+ }
+
+ let box = elements.first ?? attributes.first
+
+ if box is SingleKeyedBox {
+ return false
+ }
+
+ return box?.isNull ?? true
+ }
+
+ public func decode(
+ _ type: T.Type, forKey key: Key
+ ) throws -> T {
+ let attributeFound = container.withShared { keyedBox in
+ !keyedBox.attributes[key.stringValue].isEmpty
+ }
+
+ let elementFound = container.withShared { keyedBox in
+ !keyedBox.elements[key.stringValue].isEmpty || keyedBox.value != nil
+ }
+
+ if let type = type as? XMLDecodableSequence.Type,
+ !attributeFound,
+ !elementFound,
+ let result = type.init() as? T
+ {
+ return result
+ }
+
+ return try decodeConcrete(type, forKey: key)
+ }
+
+ public func nestedContainer(
+ keyedBy _: NestedKey.Type, forKey key: Key
+ ) throws -> KeyedDecodingContainer {
+ decoder.codingPath.append(key)
+ defer { decoder.codingPath.removeLast() }
+
+ let elements = self.container.withShared { keyedBox in
+ keyedBox.elements[key.stringValue]
+ }
+
+ let attributes = self.container.withShared { keyedBox in
+ keyedBox.attributes[key.stringValue]
+ }
+
+ guard let value = elements.first ?? attributes.first else {
+ throw DecodingError.keyNotFound(key, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription:
+ """
+ Cannot get \(KeyedDecodingContainer.self) -- \
+ no value found for key \"\(key.stringValue)\"
+ """
+ ))
+ }
+
+ guard let container = XMLKeyedDecodingContainer(box: value, decoder: decoder) else {
+ throw DecodingError.typeMismatch(
+ at: codingPath,
+ expectation: [String: Any].self,
+ reality: value
+ )
+ }
+
+ return KeyedDecodingContainer(container)
+ }
+
+ public func nestedUnkeyedContainer(
+ forKey key: Key
+ ) throws -> UnkeyedDecodingContainer {
+ decoder.codingPath.append(key)
+ defer { decoder.codingPath.removeLast() }
+
+ let elements = container.unboxed.elements[key.stringValue]
+
+ if let containsKeyed = elements as? [KeyedBox], containsKeyed.count == 1, let keyed = containsKeyed.first {
+ return XMLUnkeyedDecodingContainer(
+ referencing: decoder,
+ wrapping: SharedBox(keyed.elements.map(SingleKeyedBox.init))
+ )
+ } else {
+ return XMLUnkeyedDecodingContainer(
+ referencing: decoder,
+ wrapping: SharedBox(elements)
+ )
+ }
+ }
+
+ public func superDecoder() throws -> Decoder {
+ return try _superDecoder(forKey: XMLKey.super)
+ }
+
+ public func superDecoder(forKey key: Key) throws -> Decoder {
+ return try _superDecoder(forKey: key)
+ }
+}
+
+extension XMLKeyedDecodingContainer {
+ internal init?(box: Box, decoder: XMLDecoderImplementation) {
+ switch box {
+ case let keyedContainer as KeyedContainer:
+ self.init(
+ referencing: decoder,
+ wrapping: keyedContainer
+ )
+ case let keyedBox as KeyedBox:
+ self.init(
+ referencing: decoder,
+ wrapping: SharedBox(keyedBox)
+ )
+ case let singleBox as SingleKeyedBox:
+ let element = (singleBox.key, singleBox.element)
+ let keyedContainer = KeyedBox(elements: [element], attributes: [])
+ self.init(
+ referencing: decoder,
+ wrapping: SharedBox(keyedContainer)
+ )
+ default:
+ return nil
+ }
+ }
+}
+
+/// Private functions
+extension XMLKeyedDecodingContainer {
+ private func _errorDescription(of key: CodingKey) -> String {
+ switch decoder.options.keyDecodingStrategy {
+ case .convertFromSnakeCase:
+ // In this case we can attempt to recover the original value by
+ // reversing the transform
+ let original = key.stringValue
+ let converted = XMLEncoder.KeyEncodingStrategy
+ ._convertToSnakeCase(original)
+ if converted == original {
+ return "\(key) (\"\(original)\")"
+ } else {
+ return "\(key) (\"\(original)\"), converted to \(converted)"
+ }
+ default:
+ // Otherwise, just report the converted string
+ return "\(key) (\"\(key.stringValue)\")"
+ }
+ }
+
+ private func decodeSignedInteger(_ type: T.Type,
+ forKey key: Key) throws -> T
+ where T: BinaryInteger & SignedInteger & Decodable
+ {
+ return try decodeConcrete(type, forKey: key)
+ }
+
+ private func decodeUnsignedInteger(_ type: T.Type,
+ forKey key: Key) throws -> T
+ where T: BinaryInteger & UnsignedInteger & Decodable
+ {
+ return try decodeConcrete(type, forKey: key)
+ }
+
+ private func decodeFloatingPoint(_ type: T.Type,
+ forKey key: Key) throws -> T
+ where T: BinaryFloatingPoint & Decodable
+ {
+ return try decodeConcrete(type, forKey: key)
+ }
+
+ private func decodeConcrete(
+ _ type: T.Type,
+ forKey key: Key
+ ) throws -> T {
+ guard let strategy = decoder.nodeDecodings.last else {
+ preconditionFailure(
+ """
+ Attempt to access node decoding strategy from empty stack.
+ """
+ )
+ }
+
+ let elements = container
+ .withShared { keyedBox -> [KeyedBox.Element] in
+ return (key.isInlined ? keyedBox.elements.values : keyedBox.elements[key.stringValue]).map {
+ if let singleKeyed = $0 as? SingleKeyedBox {
+ return singleKeyed.element.isNull ? singleKeyed : singleKeyed.element
+ } else {
+ return $0
+ }
+ }
+ }
+
+ let attributes = container.withShared { keyedBox in
+ key.isInlined ? keyedBox.attributes.values : keyedBox.attributes[key.stringValue]
+ }
+
+ decoder.codingPath.append(key)
+ let nodeDecodings = decoder.options.nodeDecodingStrategy.nodeDecodings(
+ forType: T.self,
+ with: decoder
+ )
+ decoder.nodeDecodings.append(nodeDecodings)
+ defer {
+ _ = decoder.nodeDecodings.removeLast()
+ decoder.codingPath.removeLast()
+ }
+
+ // You can't decode sequences from attributes, but other strategies
+ // need special handling for empty sequences.
+ if strategy(key) != .attribute && elements.isEmpty,
+ let empty = (type as? XMLDecodableSequence.Type)?.init() as? T
+ {
+ return empty
+ }
+
+ // If we are looking at a coding key value intrinsic where the expected type is `String` and
+ // the value is empty, return CDATA if present otherwise `""`.
+ if strategy(key) != .attribute, elements.isEmpty, attributes.isEmpty, type == String.self, key.stringValue == "", let emptyString = "" as? T {
+ let cdata = container.withShared { keyedBox in
+ keyedBox.elements["#CDATA"].map {
+ return ($0 as? KeyedBox)?.value ?? $0
+ }
+ }.first
+ return ((cdata as? StringBox)?.unboxed as? T) ?? emptyString
+ }
+
+ let box: Box
+ if key.isInlined {
+ box = container.typeErasedUnbox()
+ } else {
+ switch strategy(key) {
+ case .attribute?:
+ box = try getAttributeBox(for: type, attributes, key)
+ case .element?:
+ box = try getElementBox(for: type, elements, key)
+ case .elementOrAttribute?:
+ box = try getAttributeOrElementBox(attributes, elements, key)
+ default:
+ switch type {
+ case is XMLAttributeProtocol.Type:
+ box = try getAttributeBox(for: type, attributes, key)
+ case is XMLElementProtocol.Type:
+ box = try getElementBox(for: type, elements, key)
+ default:
+ box = try getAttributeOrElementBox(attributes, elements, key)
+ }
+ }
+ }
+
+ let value: T?
+ if !(type is XMLDecodableSequence.Type), let unkeyedBox = box as? UnkeyedBox,
+ let first = unkeyedBox.first
+ {
+ // Handle case where we have held onto a `SingleKeyedBox`
+ if let singleKeyed = first as? SingleKeyedBox {
+ if singleKeyed.element.isNull {
+ value = try decoder.unbox(singleKeyed)
+ } else {
+ value = try decoder.unbox(singleKeyed.element)
+ }
+ } else {
+ value = try decoder.unbox(first)
+ }
+ } else if box.isNull, let type = type as? XMLOptionalAttributeProtocol.Type, let nullAttribute = type.init() as? T {
+ value = nullAttribute
+ } else {
+ value = try decoder.unbox(box)
+ }
+
+ if value == nil, let type = type as? AnyOptional.Type,
+ let result = type.init() as? T
+ {
+ return result
+ }
+
+ guard let unwrapped = value else {
+ throw DecodingError.valueNotFound(type, DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription:
+ "Expected \(type) value but found null instead."
+ ))
+ }
+ return unwrapped
+ }
+
+ private func getAttributeBox(for type: T.Type, _ attributes: [KeyedBox.Attribute], _ key: Key) throws -> Box {
+ if let box = attributes.first { return box }
+ if type is AnyOptional.Type || type is XMLOptionalAttributeProtocol.Type { return NullBox() }
+
+ throw DecodingError.keyNotFound(key, DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "No attribute found for key \(_errorDescription(of: key))."
+ ))
+ }
+
+ private func getElementBox(for type: T.Type, _ elements: [KeyedBox.Element], _ key: Key) throws -> Box {
+ guard elements.isEmpty else { return elements }
+ if type is AnyOptional.Type || type is XMLDecodableSequence.Type { return elements }
+
+ throw DecodingError.keyNotFound(key, DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "No element found for key \(_errorDescription(of: key))."
+ ))
+ }
+
+ private func getAttributeOrElementBox(_ attributes: [KeyedBox.Attribute], _ elements: [KeyedBox.Element], _ key: Key) throws -> Box {
+ guard
+ let anyBox = elements.isEmpty ? attributes.first : elements as Box?
+ else {
+ throw DecodingError.keyNotFound(key, DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription:
+ """
+ No attribute or element found for key \
+ \(_errorDescription(of: key)).
+ """
+ ))
+ }
+ return anyBox
+ }
+
+ private func _superDecoder(forKey key: CodingKey) throws -> Decoder {
+ decoder.codingPath.append(key)
+ defer { decoder.codingPath.removeLast() }
+
+ let elements = container.withShared { keyedBox in
+ keyedBox.elements[key.stringValue]
+ }
+
+ let attributes = container.withShared { keyedBox in
+ keyedBox.attributes[key.stringValue]
+ }
+
+ let box: Box = elements.first ?? attributes.first ?? NullBox()
+ return XMLDecoderImplementation(
+ referencing: box,
+ options: decoder.options,
+ nodeDecodings: decoder.nodeDecodings,
+ codingPath: decoder.codingPath
+ )
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Decoder/XMLUnkeyedDecodingContainer.swift b/third-party/XMLCoder/Sources/Decoder/XMLUnkeyedDecodingContainer.swift
new file mode 100644
index 0000000000..0ba8b3a204
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Decoder/XMLUnkeyedDecodingContainer.swift
@@ -0,0 +1,232 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/21/17.
+//
+
+import Foundation
+
+struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer {
+ // MARK: Properties
+
+ /// A reference to the decoder we're reading from.
+ private let decoder: XMLDecoderImplementation
+
+ /// A reference to the container we're reading from.
+ private let container: SharedBox
+
+ /// The path of coding keys taken to get to this point in decoding.
+ public private(set) var codingPath: [CodingKey]
+
+ /// The index of the element we're about to decode.
+ public private(set) var currentIndex: Int
+
+ // MARK: - Initialization
+
+ /// Initializes `self` by referencing the given decoder and container.
+ init(referencing decoder: XMLDecoderImplementation, wrapping container: SharedBox) {
+ self.decoder = decoder
+ self.container = container
+ codingPath = decoder.codingPath
+ currentIndex = 0
+ }
+
+ // MARK: - UnkeyedDecodingContainer Methods
+
+ public var count: Int? {
+ return container.withShared { unkeyedBox in
+ unkeyedBox.count
+ }
+ }
+
+ public var isAtEnd: Bool {
+ return currentIndex >= count!
+ }
+
+ public mutating func decodeNil() throws -> Bool {
+ guard !isAtEnd else {
+ throw DecodingError.valueNotFound(Any?.self, DecodingError.Context(
+ codingPath: decoder.codingPath + [XMLKey(index: currentIndex)],
+ debugDescription: "Unkeyed container is at end."
+ ))
+ }
+
+ let isNull = container.withShared { unkeyedBox in
+ unkeyedBox[self.currentIndex].isNull
+ }
+
+ if isNull {
+ currentIndex += 1
+ return true
+ } else {
+ return false
+ }
+ }
+
+ public mutating func decode(_ type: T.Type) throws -> T {
+ return try decode(type) { decoder, box in
+ try decoder.unbox(box)
+ }
+ }
+
+ private mutating func decode(
+ _ type: T.Type,
+ decode: (XMLDecoderImplementation, Box) throws -> T?
+ ) throws -> T {
+ decoder.codingPath.append(XMLKey(index: currentIndex))
+ let nodeDecodings = decoder.options.nodeDecodingStrategy.nodeDecodings(
+ forType: T.self,
+ with: decoder
+ )
+ decoder.nodeDecodings.append(nodeDecodings)
+ defer {
+ _ = decoder.nodeDecodings.removeLast()
+ _ = decoder.codingPath.removeLast()
+ }
+ guard !isAtEnd else {
+ throw DecodingError.valueNotFound(type, DecodingError.Context(
+ codingPath: decoder.codingPath + [XMLKey(index: currentIndex)],
+ debugDescription: "Unkeyed container is at end."
+ ))
+ }
+
+ decoder.codingPath.append(XMLKey(index: currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ let box = container.withShared { unkeyedBox in
+ unkeyedBox[self.currentIndex]
+ }
+
+ var value: T?
+ if let singleKeyed = box as? SingleKeyedBox {
+ do {
+ value = try decode(decoder, singleKeyed)
+ } catch {
+ do {
+ // Drill down to the element in the case of an nested unkeyed element
+ value = try decode(decoder, singleKeyed.element)
+ } catch {
+ // Specialize for choice elements
+ value = try decode(decoder, ChoiceBox(key: singleKeyed.key, element: singleKeyed.element))
+ }
+ }
+ } else {
+ value = try decode(decoder, box)
+ }
+
+ defer { currentIndex += 1 }
+
+ if value == nil, let type = type as? AnyOptional.Type,
+ let result = type.init() as? T
+ {
+ return result
+ }
+
+ guard let decoded: T = value else {
+ throw DecodingError.valueNotFound(type, DecodingError.Context(
+ codingPath: decoder.codingPath + [XMLKey(index: currentIndex)],
+ debugDescription: "Expected \(type) but found null instead."
+ ))
+ }
+
+ return decoded
+ }
+
+ public mutating func nestedContainer(
+ keyedBy _: NestedKey.Type
+ ) throws -> KeyedDecodingContainer {
+ decoder.codingPath.append(XMLKey(index: currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard !isAtEnd else {
+ throw DecodingError.valueNotFound(
+ KeyedDecodingContainer.self, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Cannot get nested keyed container -- unkeyed container is at end."
+ )
+ )
+ }
+
+ let value = self.container.withShared { unkeyedBox in
+ unkeyedBox[self.currentIndex]
+ }
+ guard !value.isNull else {
+ throw DecodingError.valueNotFound(KeyedDecodingContainer.self, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Cannot get keyed decoding container -- found null value instead."
+ ))
+ }
+
+ guard let keyedContainer = value as? SharedBox else {
+ throw DecodingError.typeMismatch(at: codingPath,
+ expectation: [String: Any].self,
+ reality: value)
+ }
+
+ currentIndex += 1
+ let container = XMLKeyedDecodingContainer(
+ referencing: decoder,
+ wrapping: keyedContainer
+ )
+ return KeyedDecodingContainer(container)
+ }
+
+ public mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
+ decoder.codingPath.append(XMLKey(index: currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard !isAtEnd else {
+ throw DecodingError.valueNotFound(
+ UnkeyedDecodingContainer.self, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Cannot get nested keyed container -- unkeyed container is at end."
+ )
+ )
+ }
+
+ let value = container.withShared { unkeyedBox in
+ unkeyedBox[self.currentIndex]
+ }
+ guard !value.isNull else {
+ throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Cannot get keyed decoding container -- found null value instead."
+ ))
+ }
+
+ guard let unkeyedContainer = value as? SharedBox else {
+ throw DecodingError.typeMismatch(at: codingPath,
+ expectation: UnkeyedBox.self,
+ reality: value)
+ }
+
+ currentIndex += 1
+ return XMLUnkeyedDecodingContainer(referencing: decoder, wrapping: unkeyedContainer)
+ }
+
+ public mutating func superDecoder() throws -> Decoder {
+ decoder.codingPath.append(XMLKey(index: currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard !isAtEnd else {
+ throw DecodingError.valueNotFound(Decoder.self, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Cannot get superDecoder() -- unkeyed container is at end."
+ ))
+ }
+
+ let value = container.withShared { unkeyedBox in
+ unkeyedBox[self.currentIndex]
+ }
+ currentIndex += 1
+
+ return XMLDecoderImplementation(
+ referencing: value,
+ options: decoder.options,
+ nodeDecodings: decoder.nodeDecodings,
+ codingPath: decoder.codingPath
+ )
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/DynamicNodeEncoding.swift b/third-party/XMLCoder/Sources/Encoder/DynamicNodeEncoding.swift
new file mode 100644
index 0000000000..bbb79fa82b
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/DynamicNodeEncoding.swift
@@ -0,0 +1,57 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Joseph Mattiello on 1/24/19.
+//
+
+/** Allows conforming types to specify how its properties will be encoded.
+
+ For example:
+ ```swift
+ struct Book: Codable, Equatable, DynamicNodeEncoding {
+ let id: UInt
+ let title: String
+ let categories: [Category]
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case title
+ case categories = "category"
+ }
+
+ static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
+ switch key {
+ case Book.CodingKeys.id: return .both
+ default: return .element
+ }
+ }
+ }
+ ```
+ produces XML of this form for values of type `Book`:
+
+ ```xml
+
+ 123
+ Cat in the Hat
+ Kids
+ Wildlife
+
+ ```
+ */
+public protocol DynamicNodeEncoding: Encodable, Sendable {
+ static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
+}
+
+extension Array: DynamicNodeEncoding where Element: DynamicNodeEncoding {
+ public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
+ return Element.nodeEncoding(for: key)
+ }
+}
+
+public extension DynamicNodeEncoding where Self: Collection, Self.Iterator.Element: DynamicNodeEncoding {
+ static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
+ return Element.nodeEncoding(for: key)
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/EncodingErrorExtension.swift b/third-party/XMLCoder/Sources/Encoder/EncodingErrorExtension.swift
new file mode 100644
index 0000000000..a364159669
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/EncodingErrorExtension.swift
@@ -0,0 +1,36 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/22/17.
+//
+
+import Foundation
+
+/// Error Utilities
+extension EncodingError {
+ /// Returns a `.invalidValue` error describing the given invalid floating-point value.
+ ///
+ ///
+ /// - parameter value: The value that was invalid to encode.
+ /// - parameter path: The path of `CodingKey`s taken to encode this value.
+ /// - returns: An `EncodingError` with the appropriate path and debug description.
+ static func _invalidFloatingPointValue(_ value: T, at codingPath: [CodingKey]) -> EncodingError {
+ let valueDescription: String
+ if value == T.infinity {
+ valueDescription = "\(T.self).infinity"
+ } else if value == -T.infinity {
+ valueDescription = "-\(T.self).infinity"
+ } else {
+ valueDescription = "\(T.self).nan"
+ }
+
+ let debugDescription = """
+ Unable to encode \(valueDescription) directly in XML. \
+ Use XMLEncoder.NonConformingFloatEncodingStrategy.convertToString \
+ to specify how the value should be encoded.
+ """
+ return .invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription))
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/SingleValueEncodingContainer.swift b/third-party/XMLCoder/Sources/Encoder/SingleValueEncodingContainer.swift
new file mode 100644
index 0000000000..2d96fdc771
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/SingleValueEncodingContainer.swift
@@ -0,0 +1,103 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/22/17.
+//
+
+import Foundation
+
+extension XMLEncoderImplementation: SingleValueEncodingContainer {
+ // MARK: - SingleValueEncodingContainer Methods
+
+ func assertCanEncodeNewValue() {
+ precondition(
+ canEncodeNewValue,
+ """
+ Attempt to encode value through single value container when \
+ previously value already encoded.
+ """
+ )
+ }
+
+ public func encodeNil() throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box())
+ }
+
+ public func encode(_ value: Bool) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: Int) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: Int8) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: Int16) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: Int32) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: Int64) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: UInt) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: UInt8) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: UInt16) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: UInt32) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: UInt64) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: String) throws {
+ assertCanEncodeNewValue()
+ storage.push(container: box(value))
+ }
+
+ public func encode(_ value: Float) throws {
+ assertCanEncodeNewValue()
+ try storage.push(container: box(value))
+ }
+
+ public func encode(_ value: Double) throws {
+ assertCanEncodeNewValue()
+ try storage.push(container: box(value))
+ }
+
+ public func encode(_ value: T) throws {
+ assertCanEncodeNewValue()
+ try storage.push(container: box(value))
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/XMLChoiceEncodingContainer.swift b/third-party/XMLCoder/Sources/Encoder/XMLChoiceEncodingContainer.swift
new file mode 100644
index 0000000000..5a07e4d1d3
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/XMLChoiceEncodingContainer.swift
@@ -0,0 +1,208 @@
+// Copyright (c) 2019-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Benjamin Wetherfield on 7/17/19.
+//
+
+struct XMLChoiceEncodingContainer: KeyedEncodingContainerProtocol {
+ typealias Key = K
+
+ // MARK: Properties
+
+ /// A reference to the encoder we're writing to.
+ private let encoder: XMLEncoderImplementation
+
+ /// A reference to the container we're writing to.
+ private var container: SharedBox
+
+ /// The path of coding keys taken to get to this point in encoding.
+ public private(set) var codingPath: [CodingKey]
+
+ // MARK: - Initialization
+
+ /// Initializes `self` with the given references.
+ init(
+ referencing encoder: XMLEncoderImplementation,
+ codingPath: [CodingKey],
+ wrapping container: SharedBox
+ ) {
+ self.encoder = encoder
+ self.codingPath = codingPath
+ self.container = container
+ }
+
+ // MARK: - Coding Path Operations
+
+ private func converted(_ key: CodingKey) -> CodingKey {
+ switch encoder.options.keyEncodingStrategy {
+ case .useDefaultKeys:
+ return key
+ case .convertToSnakeCase:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToSnakeCase(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ case .convertToKebabCase:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToKebabCase(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ case let .custom(converter):
+ return converter(codingPath + [key])
+ case .capitalized:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToCapitalized(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ case .uppercased:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToUppercased(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ case .lowercased:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToLowercased(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ }
+ }
+
+ // MARK: - KeyedEncodingContainerProtocol Methods
+
+ public mutating func encodeNil(forKey key: Key) throws {
+ container.withShared {
+ $0.key = converted(key).stringValue
+ $0.element = NullBox()
+ }
+ }
+
+ public mutating func encode(
+ _ value: T,
+ forKey key: Key
+ ) throws {
+ return try encode(value, forKey: key) { encoder, value in
+ try encoder.box(value)
+ }
+ }
+
+ private mutating func encode(
+ _ value: T,
+ forKey key: Key,
+ encode: (XMLEncoderImplementation, T) throws -> Box
+ ) throws {
+ defer {
+ _ = self.encoder.nodeEncodings.removeLast()
+ self.encoder.codingPath.removeLast()
+ }
+ encoder.codingPath.append(key)
+ let nodeEncodings = encoder.options.nodeEncodingStrategy.nodeEncodings(
+ forType: T.self,
+ with: encoder
+ )
+ encoder.nodeEncodings.append(nodeEncodings)
+ let box = try encode(encoder, value)
+
+ let oldSelf = self
+ let elementEncoder: (T, Key, Box) throws -> () = { _, key, box in
+ oldSelf.container.withShared { container in
+ container.element = box
+ container.key = oldSelf.converted(key).stringValue
+ }
+ }
+
+ defer {
+ self = oldSelf
+ }
+
+ try elementEncoder(value, key, box)
+ }
+
+ public mutating func nestedContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ if NestedKey.self is XMLChoiceCodingKey.Type {
+ return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key)
+ } else {
+ return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key)
+ }
+ }
+
+ mutating func nestedKeyedContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ let sharedKeyed = SharedBox(KeyedBox())
+
+ self.container.withShared { container in
+ container.element = sharedKeyed
+ container.key = converted(key).stringValue
+ }
+
+ codingPath.append(key)
+ defer { self.codingPath.removeLast() }
+
+ let container = XMLKeyedEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedKeyed
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ mutating func nestedChoiceContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ let sharedChoice = SharedBox(ChoiceBox())
+
+ self.container.withShared { container in
+ container.element = sharedChoice
+ container.key = converted(key).stringValue
+ }
+
+ codingPath.append(key)
+ defer { self.codingPath.removeLast() }
+
+ let container = XMLChoiceEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedChoice
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ public mutating func nestedUnkeyedContainer(
+ forKey key: Key
+ ) -> UnkeyedEncodingContainer {
+ let sharedUnkeyed = SharedBox(UnkeyedBox())
+
+ container.withShared { container in
+ container.element = sharedUnkeyed
+ container.key = converted(key).stringValue
+ }
+
+ codingPath.append(key)
+ defer { self.codingPath.removeLast() }
+ return XMLUnkeyedEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedUnkeyed
+ )
+ }
+
+ public mutating func superEncoder() -> Encoder {
+ return XMLReferencingEncoder(
+ referencing: encoder,
+ key: XMLKey.super,
+ convertedKey: converted(XMLKey.super),
+ wrapping: container
+ )
+ }
+
+ public mutating func superEncoder(forKey key: Key) -> Encoder {
+ return XMLReferencingEncoder(
+ referencing: encoder,
+ key: key,
+ convertedKey: converted(key),
+ wrapping: container
+ )
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/XMLEncoder.swift b/third-party/XMLCoder/Sources/Encoder/XMLEncoder.swift
new file mode 100644
index 0000000000..3444c7330e
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/XMLEncoder.swift
@@ -0,0 +1,451 @@
+// Copyright © 2017-2021 Shawn Moore and XMLCoder contributors.
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/22/17.
+//
+
+import Foundation
+
+/// `XMLEncoder` facilitates the encoding of `Encodable` values into XML.
+open class XMLEncoder {
+ // MARK: Options
+
+ /// The formatting of the output XML data.
+ public struct OutputFormatting: OptionSet, Sendable {
+ /// The format's default value.
+ public let rawValue: UInt
+
+ /// Creates an OutputFormatting value with the given raw value.
+ public init(rawValue: UInt) {
+ self.rawValue = rawValue
+ }
+
+ /// Produce human-readable XML with indented output.
+ public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0)
+
+ /// Produce XML with keys sorted in lexicographic order.
+ public static let sortedKeys = OutputFormatting(rawValue: 1 << 1)
+
+ /// Produce XML with no short-hand annotation for empty elements, e.g., use `` over `
`
+ public static let noEmptyElements = OutputFormatting(rawValue: 1 << 2)
+ }
+
+ /// The indentation to use when XML is pretty-printed.
+ public enum PrettyPrintIndentation {
+ case spaces(Int)
+ case tabs(Int)
+ }
+
+ /// A node's encoding type. Specifies how a node will be encoded.
+ public enum NodeEncoding : Sendable {
+ case attribute
+ case element
+ case both
+
+ public static let `default`: NodeEncoding = .element
+ }
+
+ /// The strategy to use for encoding `Date` values.
+ public enum DateEncodingStrategy {
+ /// Defer to `Date` for choosing an encoding. This is the default strategy.
+ case deferredToDate
+
+ /// Encode the `Date` as a UNIX timestamp (as a XML number).
+ case secondsSince1970
+
+ /// Encode the `Date` as UNIX millisecond timestamp (as a XML number).
+ case millisecondsSince1970
+
+ /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
+ @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
+ case iso8601
+
+ /// Encode the `Date` as a string formatted by the given formatter.
+ case formatted(DateFormatter)
+
+ /// Encode the `Date` as a custom value encoded by the given closure.
+ ///
+ /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
+ case custom((Date, Encoder) throws -> ())
+ }
+
+ /// The strategy to use for encoding `String` values.
+ public enum StringEncodingStrategy {
+ /// Defer to `String` for choosing an encoding. This is the default strategy.
+ case deferredToString
+
+ /// Encode the `String` as a CData-encoded string.
+ case cdata
+ }
+
+ /// The strategy to use for encoding `Data` values.
+ public enum DataEncodingStrategy {
+ /// Defer to `Data` for choosing an encoding.
+ case deferredToData
+
+ /// Encoded the `Data` as a Base64-encoded string. This is the default strategy.
+ case base64
+
+ /// Encode the `Data` as a custom value encoded by the given closure.
+ ///
+ /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
+ case custom((Data, Encoder) throws -> ())
+ }
+
+ /// The strategy to use for non-XML-conforming floating-point values (IEEE 754 infinity and NaN).
+ public enum NonConformingFloatEncodingStrategy {
+ /// Throw upon encountering non-conforming values. This is the default strategy.
+ case `throw`
+
+ /// Encode the values using the given representation strings.
+ case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String)
+ }
+
+ /// The strategy to use for automatically changing the value of keys before encoding.
+ public enum KeyEncodingStrategy {
+ /// Use the keys specified by each type. This is the default strategy.
+ case useDefaultKeys
+
+ /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to XML payload.
+ ///
+ /// Capital characters are determined by testing membership in
+ /// `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters`
+ /// (Unicode General Categories Lu and Lt).
+ /// The conversion to lower case uses `Locale.system`, also known as
+ /// the ICU "root" locale. This means the result is consistent
+ /// regardless of the current user's locale and language preferences.
+ ///
+ /// Converting from camel case to snake case:
+ /// 1. Splits words at the boundary of lower-case to upper-case
+ /// 2. Inserts `_` between words
+ /// 3. Lowercases the entire string
+ /// 4. Preserves starting and ending `_`.
+ ///
+ /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.
+ ///
+ /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.
+ case convertToSnakeCase
+
+ /// Same as convertToSnakeCase, but using `-` instead of `_`
+ /// For example, `oneTwoThree` becomes `one-two-three`.
+ case convertToKebabCase
+
+ /// Capitalize the first letter only
+ /// `oneTwoThree` becomes `OneTwoThree`
+ case capitalized
+
+ /// Uppercase ize all letters
+ /// `oneTwoThree` becomes `ONETWOTHREE`
+ case uppercased
+
+ /// Lowercase all letters
+ /// `oneTwoThree` becomes `onetwothree`
+ case lowercased
+
+ /// Provide a custom conversion to the key in the encoded XML from the
+ /// keys specified by the encoded types.
+ /// The full path to the current encoding position is provided for
+ /// context (in case you need to locate this key within the payload).
+ /// The returned key is used in place of the last component in the
+ /// coding path before encoding.
+ /// If the result of the conversion is a duplicate key, then only one
+ /// value will be present in the result.
+ case custom((_ codingPath: [CodingKey]) -> CodingKey)
+
+ static func _convertToSnakeCase(_ stringKey: String) -> String {
+ return _convert(stringKey, usingSeparator: "_")
+ }
+
+ static func _convertToKebabCase(_ stringKey: String) -> String {
+ return _convert(stringKey, usingSeparator: "-")
+ }
+
+ static func _convert(_ stringKey: String, usingSeparator separator: String) -> String {
+ guard !stringKey.isEmpty else {
+ return stringKey
+ }
+
+ var words: [Range] = []
+ // The general idea of this algorithm is to split words on
+ // transition from lower to upper case, then on transition of >1
+ // upper case characters to lowercase
+ //
+ // myProperty -> my_property
+ // myURLProperty -> my_url_property
+ //
+ // We assume, per Swift naming conventions, that the first character of the key is lowercase.
+ var wordStart = stringKey.startIndex
+ var searchRange = stringKey.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
+ let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
+ words.append(upperCaseRange.lowerBound.. String {
+ return stringKey.capitalizingFirstLetter()
+ }
+
+ static func _convertToLowercased(_ stringKey: String) -> String {
+ return stringKey.lowercased()
+ }
+
+ static func _convertToUppercased(_ stringKey: String) -> String {
+ return stringKey.uppercased()
+ }
+ }
+
+ @available(*, deprecated, renamed: "NodeEncodingStrategy")
+ public typealias NodeEncodingStrategies = NodeEncodingStrategy
+
+ public typealias XMLNodeEncoderClosure = @Sendable (CodingKey) -> NodeEncoding?
+ public typealias XMLEncodingClosure = @Sendable (Encodable.Type, Encoder) -> XMLNodeEncoderClosure
+
+ /// Set of strategies to use for encoding of nodes.
+ public enum NodeEncodingStrategy {
+ /// Defer to `Encoder` for choosing an encoding. This is the default strategy.
+ case deferredToEncoder
+
+ /// Return a closure computing the desired node encoding for the value by its coding key.
+ case custom(XMLEncodingClosure)
+
+ func nodeEncodings(
+ forType codableType: Encodable.Type,
+ with encoder: Encoder
+ ) -> ((CodingKey) -> NodeEncoding?) {
+ return encoderClosure(codableType, encoder)
+ }
+
+ var encoderClosure: XMLEncodingClosure {
+ switch self {
+ case .deferredToEncoder: return NodeEncodingStrategy.defaultEncoder
+ case let .custom(closure): return closure
+ }
+ }
+
+ static let defaultEncoder: XMLEncodingClosure = { codableType, _ in
+ guard let dynamicType = codableType as? DynamicNodeEncoding.Type else {
+ return { _ in nil }
+ }
+ #if compiler(>=6.1)
+ return dynamicType.nodeEncoding(for:)
+ #else
+ return { (@Sendable key: CodingKey) in
+ dynamicType.nodeEncoding(for: key)
+ }
+ #endif
+ }
+ }
+
+ /// Characters and their escaped representations to be escaped in attributes
+ open var charactersEscapedInAttributes = [
+ ("&", "&"),
+ ("<", "<"),
+ (">", ">"),
+ ("'", "'"),
+ ("\"", """),
+ ]
+
+ /// Characters and their escaped representations to be escaped in elements
+ open var charactersEscapedInElements = [
+ ("&", "&"),
+ ("<", "<"),
+ (">", ">"),
+ ("'", "'"),
+ ("\"", """),
+ ]
+
+ /// The output format to produce. Defaults to `[]`.
+ open var outputFormatting: OutputFormatting = []
+
+ /// The indentation to use when XML is printed. Defaults to `.spaces(4)`.
+ open var prettyPrintIndentation: PrettyPrintIndentation = .spaces(4)
+
+ /// The strategy to use in encoding dates. Defaults to `.deferredToDate`.
+ open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate
+
+ /// The strategy to use in encoding binary data. Defaults to `.base64`.
+ open var dataEncodingStrategy: DataEncodingStrategy = .base64
+
+ /// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`.
+ open var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw
+
+ /// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`.
+ open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
+
+ /// The strategy to use in encoding encoding attributes. Defaults to `.deferredToEncoder`.
+ open var nodeEncodingStrategy: NodeEncodingStrategy = .deferredToEncoder
+
+ /// The strategy to use in encoding strings. Defaults to `.deferredToString`.
+ open var stringEncodingStrategy: StringEncodingStrategy = .deferredToString
+
+ /// Contextual user-provided information for use during encoding.
+ open var userInfo: [CodingUserInfoKey: Any] = [:]
+
+ /// Options set on the top-level encoder to pass down the encoding hierarchy.
+ struct Options {
+ let dateEncodingStrategy: DateEncodingStrategy
+ let dataEncodingStrategy: DataEncodingStrategy
+ let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy
+ let keyEncodingStrategy: KeyEncodingStrategy
+ let nodeEncodingStrategy: NodeEncodingStrategy
+ let stringEncodingStrategy: StringEncodingStrategy
+ let userInfo: [CodingUserInfoKey: Any]
+ }
+
+ /// The options set on the top-level encoder.
+ var options: Options {
+ return Options(dateEncodingStrategy: dateEncodingStrategy,
+ dataEncodingStrategy: dataEncodingStrategy,
+ nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy,
+ keyEncodingStrategy: keyEncodingStrategy,
+ nodeEncodingStrategy: nodeEncodingStrategy,
+ stringEncodingStrategy: stringEncodingStrategy,
+ userInfo: userInfo)
+ }
+
+ // MARK: - Constructing a XML Encoder
+
+ /// Initializes `self` with default strategies.
+ public init() {}
+
+ // MARK: - Encoding Values
+
+ /// Encodes the given top-level value and returns its XML representation.
+ ///
+ /// - parameter value: The value to encode.
+ /// - parameter withRootKey: the key used to wrap the encoded values. The
+ /// default value is inferred from the name of the root type.
+ /// - parameter rootAttributes: the list of attributes to be added to the root node
+ /// - parameter header: the XML header to start the encoded data with.
+ /// - returns: A new `Data` value containing the encoded XML data.
+ /// - throws: `EncodingError.invalidValue` if a non-conforming
+ /// floating-point value is encountered during encoding, and the encoding
+ /// strategy is `.throw`.
+ /// - throws: An error if any value throws an error during encoding.
+ open func encode(_ value: T,
+ withRootKey rootKey: String? = nil,
+ rootAttributes: [String: String]? = nil,
+ header: XMLHeader? = nil,
+ doctype: XMLDocumentType? = nil) throws -> Data
+ {
+ let encoder = XMLEncoderImplementation(options: options, nodeEncodings: [])
+ encoder.nodeEncodings.append(options.nodeEncodingStrategy.nodeEncodings(forType: T.self, with: encoder))
+
+ let topLevel = try encoder.box(value)
+ let attributes = rootAttributes?.map(XMLCoderElement.Attribute.init) ?? []
+
+ let elementOrNone: XMLCoderElement?
+
+ let rootKey = rootKey ?? "\(T.self)".convert(for: keyEncodingStrategy)
+
+ let isStringBoxCDATA = stringEncodingStrategy == .cdata
+
+ if let keyedBox = topLevel as? KeyedBox {
+ elementOrNone = XMLCoderElement(
+ key: rootKey,
+ isStringBoxCDATA: isStringBoxCDATA,
+ box: keyedBox,
+ attributes: attributes
+ )
+ } else if let unkeyedBox = topLevel as? UnkeyedBox {
+ elementOrNone = XMLCoderElement(
+ key: rootKey,
+ isStringBoxCDATA: isStringBoxCDATA,
+ box: unkeyedBox,
+ attributes: attributes
+ )
+ } else if let choiceBox = topLevel as? ChoiceBox {
+ elementOrNone = XMLCoderElement(
+ key: rootKey,
+ isStringBoxCDATA: isStringBoxCDATA,
+ box: choiceBox,
+ attributes: attributes
+ )
+ } else {
+ fatalError("Unrecognized top-level element of type: \(type(of: topLevel))")
+ }
+
+ guard let element = elementOrNone else {
+ throw EncodingError.invalidValue(value, EncodingError.Context(
+ codingPath: [],
+ debugDescription: "Unable to encode the given top-level value to XML."
+ ))
+ }
+
+ return element.toXMLString(
+ with: header,
+ doctype: doctype,
+ escapedCharacters: (
+ elements: charactersEscapedInElements,
+ attributes: charactersEscapedInAttributes
+ ),
+ formatting: outputFormatting,
+ indentation: prettyPrintIndentation
+ ).data(using: .utf8, allowLossyConversion: true)!
+ }
+
+ // MARK: - TopLevelEncoder
+
+ #if canImport(Combine) || canImport(OpenCombine)
+ open func encode(_ value: T) throws -> Data where T: Encodable {
+ return try encode(value, withRootKey: nil, rootAttributes: nil, header: nil)
+ }
+ #endif
+}
+
+private extension String {
+ func convert(for encodingStrategy: XMLEncoder.KeyEncodingStrategy) -> String {
+ switch encodingStrategy {
+ case .useDefaultKeys:
+ return self
+ case .convertToSnakeCase:
+ return XMLEncoder.KeyEncodingStrategy._convertToSnakeCase(self)
+ case .convertToKebabCase:
+ return XMLEncoder.KeyEncodingStrategy._convertToKebabCase(self)
+ case .custom:
+ return self
+ case .capitalized:
+ return XMLEncoder.KeyEncodingStrategy._convertToCapitalized(self)
+ case .uppercased:
+ return XMLEncoder.KeyEncodingStrategy._convertToUppercased(self)
+ case .lowercased:
+ return XMLEncoder.KeyEncodingStrategy._convertToLowercased(self)
+ }
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/XMLEncoderImplementation.swift b/third-party/XMLCoder/Sources/Encoder/XMLEncoderImplementation.swift
new file mode 100644
index 0000000000..94e880cb2c
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/XMLEncoderImplementation.swift
@@ -0,0 +1,292 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/22/17.
+//
+
+import Foundation
+
+class XMLEncoderImplementation: Encoder {
+ // MARK: Properties
+
+ /// The encoder's storage.
+ var storage: XMLEncodingStorage
+
+ /// Options set on the top-level encoder.
+ let options: XMLEncoder.Options
+
+ /// The path to the current point in encoding.
+ public var codingPath: [CodingKey]
+
+ public var nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding?]
+
+ /// Contextual user-provided information for use during encoding.
+ public var userInfo: [CodingUserInfoKey: Any] {
+ return options.userInfo
+ }
+
+ // MARK: - Initialization
+
+ /// Initializes `self` with the given top-level encoder options.
+ init(
+ options: XMLEncoder.Options,
+ nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding?],
+ codingPath: [CodingKey] = []
+ ) {
+ self.options = options
+ storage = XMLEncodingStorage()
+ self.codingPath = codingPath
+ self.nodeEncodings = nodeEncodings
+ }
+
+ /// Returns whether a new element can be encoded at this coding path.
+ ///
+ /// `true` if an element has not yet been encoded at this coding path; `false` otherwise.
+ var canEncodeNewValue: Bool {
+ // Every time a new value gets encoded, the key it's encoded for is
+ // pushed onto the coding path (even if it's a nil key from an unkeyed container).
+ // At the same time, every time a container is requested, a new value
+ // gets pushed onto the storage stack.
+ // If there are more values on the storage stack than on the coding path,
+ // it means the value is requesting more than one container, which
+ // violates the precondition.
+ //
+ // This means that anytime something that can request a new container
+ // goes onto the stack, we MUST push a key onto the coding path.
+ // Things which will not request containers do not need to have the
+ // coding path extended for them (but it doesn't matter if it is,
+ // because they will not reach here).
+ return storage.count == codingPath.count
+ }
+
+ // MARK: - Encoder Methods
+
+ public func container(keyedBy _: Key.Type) -> KeyedEncodingContainer {
+ guard canEncodeNewValue else {
+ return mergeWithExistingKeyedContainer(keyedBy: Key.self)
+ }
+ if Key.self is XMLChoiceCodingKey.Type {
+ return choiceContainer(keyedBy: Key.self)
+ } else {
+ return keyedContainer(keyedBy: Key.self)
+ }
+ }
+
+ public func unkeyedContainer() -> UnkeyedEncodingContainer {
+ // If an existing unkeyed container was already requested, return that one.
+ let topContainer: SharedBox
+ if canEncodeNewValue {
+ // We haven't yet pushed a container at this level; do so here.
+ topContainer = storage.pushUnkeyedContainer()
+ } else {
+ guard let container = storage.lastContainer as? SharedBox else {
+ preconditionFailure(
+ """
+ Attempt to push new unkeyed encoding container when already previously encoded \
+ at this path.
+ """
+ )
+ }
+
+ topContainer = container
+ }
+
+ return XMLUnkeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer)
+ }
+
+ public func singleValueContainer() -> SingleValueEncodingContainer {
+ return self
+ }
+
+ private func keyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer {
+ let container = XMLKeyedEncodingContainer(
+ referencing: self,
+ codingPath: codingPath,
+ wrapping: storage.pushKeyedContainer()
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ private func choiceContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer {
+ let container = XMLChoiceEncodingContainer(
+ referencing: self,
+ codingPath: codingPath,
+ wrapping: storage.pushChoiceContainer()
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ private func mergeWithExistingKeyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer {
+ switch storage.lastContainer {
+ case let keyed as SharedBox:
+ let container = XMLKeyedEncodingContainer(
+ referencing: self,
+ codingPath: codingPath,
+ wrapping: keyed
+ )
+ return KeyedEncodingContainer(container)
+ case let choice as SharedBox:
+ _ = storage.popContainer()
+ let keyed = KeyedBox(
+ elements: KeyedBox.Elements([choice.withShared { ($0.key, $0.element) }]),
+ attributes: []
+ )
+ let container = XMLKeyedEncodingContainer(
+ referencing: self,
+ codingPath: codingPath,
+ wrapping: storage.pushKeyedContainer(keyed)
+ )
+ return KeyedEncodingContainer(container)
+ default:
+ preconditionFailure(
+ """
+ No existing keyed encoding container to merge with.
+ """
+ )
+ }
+ }
+}
+
+extension XMLEncoderImplementation {
+ /// Returns the given value boxed in a container appropriate for pushing onto the container stack.
+ func box() -> SimpleBox {
+ return NullBox()
+ }
+
+ func box(_ value: Bool) -> SimpleBox {
+ return BoolBox(value)
+ }
+
+ func box(_ value: Decimal) -> SimpleBox {
+ return DecimalBox(value)
+ }
+
+ func box(_ value: T) -> SimpleBox {
+ return IntBox(value)
+ }
+
+ func box(_ value: T) -> SimpleBox {
+ return UIntBox(value)
+ }
+
+ func box(_ value: Float) throws -> SimpleBox {
+ return try box(value, FloatBox.self)
+ }
+
+ func box(_ value: Double) throws -> SimpleBox {
+ return try box(value, DoubleBox.self)
+ }
+
+ func box(
+ _ value: T,
+ _: B.Type
+ ) throws -> SimpleBox where B.Unboxed == T {
+ guard value.isInfinite || value.isNaN else {
+ return B(value)
+ }
+ guard case let .convertToString(
+ positiveInfinity: posInfString,
+ negativeInfinity: negInfString,
+ nan: nanString
+ ) = options.nonConformingFloatEncodingStrategy else {
+ throw EncodingError._invalidFloatingPointValue(value, at: codingPath)
+ }
+ if value == T.infinity {
+ return StringBox(posInfString)
+ } else if value == -T.infinity {
+ return StringBox(negInfString)
+ } else {
+ return StringBox(nanString)
+ }
+ }
+
+ func box(_ value: String) -> SimpleBox {
+ return StringBox(value)
+ }
+
+ func box(_ value: Date) throws -> Box {
+ switch options.dateEncodingStrategy {
+ case .deferredToDate:
+ try value.encode(to: self)
+ return storage.popContainer()
+ case .secondsSince1970:
+ return DateBox(value, format: .secondsSince1970)
+ case .millisecondsSince1970:
+ return DateBox(value, format: .millisecondsSince1970)
+ case .iso8601:
+ return DateBox(value, format: .iso8601)
+ case let .formatted(formatter):
+ return DateBox(value, format: .formatter(formatter))
+ case let .custom(closure):
+ let depth = storage.count
+ try closure(value, self)
+
+ guard storage.count > depth else {
+ return KeyedBox()
+ }
+
+ return storage.popContainer()
+ }
+ }
+
+ func box(_ value: Data) throws -> Box {
+ switch options.dataEncodingStrategy {
+ case .deferredToData:
+ try value.encode(to: self)
+ return storage.popContainer()
+ case .base64:
+ return DataBox(value, format: .base64)
+ case let .custom(closure):
+ let depth = storage.count
+ try closure(value, self)
+
+ guard storage.count > depth else {
+ return KeyedBox()
+ }
+
+ return storage.popContainer()
+ }
+ }
+
+ func box(_ value: URL) -> SimpleBox {
+ return URLBox(value)
+ }
+
+ func box(_ value: T) throws -> Box {
+ if T.self == Date.self || T.self == NSDate.self,
+ let value = value as? Date
+ {
+ return try box(value)
+ } else if T.self == Data.self || T.self == NSData.self,
+ let value = value as? Data
+ {
+ return try box(value)
+ } else if T.self == URL.self || T.self == NSURL.self,
+ let value = value as? URL
+ {
+ return box(value)
+ } else if T.self == Decimal.self || T.self == NSDecimalNumber.self,
+ let value = value as? Decimal
+ {
+ return box(value)
+ }
+
+ let depth = storage.count
+ try value.encode(to: self)
+
+ // The top container should be a new container.
+ guard storage.count > depth else {
+ return KeyedBox()
+ }
+
+ let lastContainer = storage.popContainer()
+
+ guard let sharedBox = lastContainer as? TypeErasedSharedBoxProtocol else {
+ return lastContainer
+ }
+
+ return sharedBox.typeErasedUnbox()
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/XMLEncodingStorage.swift b/third-party/XMLCoder/Sources/Encoder/XMLEncodingStorage.swift
new file mode 100644
index 0000000000..9a33a9a1eb
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/XMLEncodingStorage.swift
@@ -0,0 +1,66 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/22/17.
+//
+
+import Foundation
+
+// MARK: - Encoding Storage and Containers
+
+struct XMLEncodingStorage {
+ // MARK: Properties
+
+ /// The container stack.
+ private var containers: [Box] = []
+
+ // MARK: - Initialization
+
+ /// Initializes `self` with no containers.
+ init() {}
+
+ // MARK: - Modifying the Stack
+
+ var count: Int {
+ return containers.count
+ }
+
+ var lastContainer: Box? {
+ return containers.last
+ }
+
+ mutating func pushKeyedContainer(_ keyedBox: KeyedBox = KeyedBox()) -> SharedBox {
+ let container = SharedBox(keyedBox)
+ containers.append(container)
+ return container
+ }
+
+ mutating func pushChoiceContainer() -> SharedBox {
+ let container = SharedBox(ChoiceBox())
+ containers.append(container)
+ return container
+ }
+
+ mutating func pushUnkeyedContainer() -> SharedBox {
+ let container = SharedBox(UnkeyedBox())
+ containers.append(container)
+ return container
+ }
+
+ mutating func push(container: Box) {
+ if let keyedBox = container as? KeyedBox {
+ containers.append(SharedBox(keyedBox))
+ } else if let unkeyedBox = container as? UnkeyedBox {
+ containers.append(SharedBox(unkeyedBox))
+ } else {
+ containers.append(container)
+ }
+ }
+
+ mutating func popContainer() -> Box {
+ precondition(!containers.isEmpty, "Empty container stack.")
+ return containers.popLast()!
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/XMLKeyedEncodingContainer.swift b/third-party/XMLCoder/Sources/Encoder/XMLKeyedEncodingContainer.swift
new file mode 100644
index 0000000000..441d7ff40b
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/XMLKeyedEncodingContainer.swift
@@ -0,0 +1,267 @@
+// Copyright (c) 2018-2021 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 11/20/18.
+//
+
+import Foundation
+
+struct XMLKeyedEncodingContainer: KeyedEncodingContainerProtocol {
+ typealias Key = K
+
+ // MARK: Properties
+
+ /// A reference to the encoder we're writing to.
+ private let encoder: XMLEncoderImplementation
+
+ /// A reference to the container we're writing to.
+ private var container: SharedBox
+
+ /// The path of coding keys taken to get to this point in encoding.
+ public private(set) var codingPath: [CodingKey]
+
+ // MARK: - Initialization
+
+ /// Initializes `self` with the given references.
+ init(
+ referencing encoder: XMLEncoderImplementation,
+ codingPath: [CodingKey],
+ wrapping container: SharedBox
+ ) {
+ self.encoder = encoder
+ self.codingPath = codingPath
+ self.container = container
+ }
+
+ // MARK: - Coding Path Operations
+
+ private func converted(_ key: CodingKey) -> CodingKey {
+ switch encoder.options.keyEncodingStrategy {
+ case .useDefaultKeys:
+ return key
+ case .convertToSnakeCase:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToSnakeCase(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ case .convertToKebabCase:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToKebabCase(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ case let .custom(converter):
+ return converter(codingPath + [key])
+ case .capitalized:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToCapitalized(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ case .uppercased:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToUppercased(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ case .lowercased:
+ let newKeyString = XMLEncoder.KeyEncodingStrategy
+ ._convertToLowercased(key.stringValue)
+ return XMLKey(stringValue: newKeyString, intValue: key.intValue)
+ }
+ }
+
+ // MARK: - KeyedEncodingContainerProtocol Methods
+
+ public mutating func encodeNil(forKey key: Key) throws {
+ container.withShared {
+ $0.elements.append(NullBox(), at: converted(key).stringValue)
+ }
+ }
+
+ public mutating func encode(
+ _ value: T,
+ forKey key: Key
+ ) throws {
+ return try encode(value, forKey: key) { encoder, value in
+ try encoder.box(value)
+ }
+ }
+
+ private mutating func encode(
+ _ value: T,
+ forKey key: Key,
+ encode: (XMLEncoderImplementation, T) throws -> Box
+ ) throws {
+ defer {
+ _ = self.encoder.nodeEncodings.removeLast()
+ self.encoder.codingPath.removeLast()
+ }
+ guard let strategy = encoder.nodeEncodings.last else {
+ preconditionFailure(
+ "Attempt to access node encoding strategy from empty stack."
+ )
+ }
+ encoder.codingPath.append(key)
+ let nodeEncodings = encoder.options.nodeEncodingStrategy.nodeEncodings(
+ forType: T.self,
+ with: encoder
+ )
+ encoder.nodeEncodings.append(nodeEncodings)
+ let box = try encode(encoder, value)
+
+ let oldSelf = self
+ let attributeEncoder: (T, Key, Box) throws -> () = { value, key, box in
+ guard let attribute = box as? SimpleBox else {
+ throw EncodingError.invalidValue(value, EncodingError.Context(
+ codingPath: [],
+ debugDescription: "Complex values cannot be encoded as attributes."
+ ))
+ }
+ oldSelf.container.withShared { container in
+ container.attributes.append(attribute, at: oldSelf.converted(key).stringValue)
+ }
+ }
+
+ let elementEncoder: (T, Key, Box) throws -> () = { _, key, box in
+ oldSelf.container.withShared { container in
+ container.elements.append(box, at: oldSelf.converted(key).stringValue)
+ }
+ }
+
+ defer {
+ self = oldSelf
+ }
+
+ switch strategy(key) {
+ case .attribute?:
+ try attributeEncoder(value, key, box)
+ case .element?:
+ try elementEncoder(value, key, box)
+ case .both?:
+ try attributeEncoder(value, key, box)
+ try elementEncoder(value, key, box)
+ default:
+ switch value {
+ case is XMLElementProtocol:
+ encodeElement(forKey: key, box: box)
+ case is XMLAttributeProtocol:
+ try encodeAttribute(value, forKey: key, box: box)
+ case is XMLElementAndAttributeProtocol:
+ try encodeAttribute(value, forKey: key, box: box)
+ encodeElement(forKey: key, box: box)
+ default:
+ encodeElement(forKey: key, box: box)
+ }
+ }
+ }
+
+ private mutating func encodeAttribute(
+ _ value: T,
+ forKey key: Key,
+ box: Box
+ ) throws {
+ guard let attribute = box as? SimpleBox else {
+ throw EncodingError.invalidValue(value, EncodingError.Context(
+ codingPath: [],
+ debugDescription: "Complex values cannot be encoded as attributes."
+ ))
+ }
+ container.withShared { container in
+ container.attributes.append(attribute, at: self.converted(key).stringValue)
+ }
+ }
+
+ private mutating func encodeElement(
+ forKey key: Key,
+ box: Box
+ ) {
+ container.withShared { container in
+ container.elements.append(box, at: self.converted(key).stringValue)
+ }
+ }
+
+ public mutating func nestedContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ if NestedKey.self is XMLChoiceCodingKey.Type {
+ return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key)
+ } else {
+ return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key)
+ }
+ }
+
+ mutating func nestedKeyedContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ let sharedKeyed = SharedBox(KeyedBox())
+
+ self.container.withShared { container in
+ container.elements.append(sharedKeyed, at: converted(key).stringValue)
+ }
+
+ codingPath.append(key)
+ defer { self.codingPath.removeLast() }
+
+ let container = XMLKeyedEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedKeyed
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ mutating func nestedChoiceContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ let sharedChoice = SharedBox(ChoiceBox())
+
+ self.container.withShared { container in
+ container.elements.append(sharedChoice, at: converted(key).stringValue)
+ }
+
+ codingPath.append(key)
+ defer { self.codingPath.removeLast() }
+
+ let container = XMLChoiceEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedChoice
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ public mutating func nestedUnkeyedContainer(
+ forKey key: Key
+ ) -> UnkeyedEncodingContainer {
+ let sharedUnkeyed = SharedBox(UnkeyedBox())
+
+ container.withShared { container in
+ container.elements.append(sharedUnkeyed, at: converted(key).stringValue)
+ }
+
+ codingPath.append(key)
+ defer { self.codingPath.removeLast() }
+ return XMLUnkeyedEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedUnkeyed
+ )
+ }
+
+ public mutating func superEncoder() -> Encoder {
+ return XMLReferencingEncoder(
+ referencing: encoder,
+ key: XMLKey.super,
+ convertedKey: converted(XMLKey.super),
+ wrapping: container
+ )
+ }
+
+ public mutating func superEncoder(forKey key: Key) -> Encoder {
+ return XMLReferencingEncoder(
+ referencing: encoder,
+ key: key,
+ convertedKey: converted(key),
+ wrapping: container
+ )
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/XMLReferencingEncoder.swift b/third-party/XMLCoder/Sources/Encoder/XMLReferencingEncoder.swift
new file mode 100644
index 0000000000..ff88334717
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/XMLReferencingEncoder.swift
@@ -0,0 +1,130 @@
+// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Shawn Moore on 11/25/17.
+//
+
+import Foundation
+
+/// XMLReferencingEncoder is a special subclass of _XMLEncoder which has its
+/// own storage, but references the contents of a different encoder.
+/// It's used in superEncoder(), which returns a new encoder for encoding a
+// superclass -- the lifetime of the encoder should not escape the scope it's
+/// created in, but it doesn't necessarily know when it's done being used
+/// (to write to the original container).
+class XMLReferencingEncoder: XMLEncoderImplementation {
+ // MARK: Reference types.
+
+ /// The type of container we're referencing.
+ private enum Reference {
+ /// Referencing a specific index in an unkeyed container.
+ case unkeyed(SharedBox, Int)
+
+ /// Referencing a specific key in a keyed container.
+ case keyed(SharedBox, String)
+
+ /// Referencing a specific key in a keyed container.
+ case choice(SharedBox, String)
+ }
+
+ // MARK: - Properties
+
+ /// The encoder we're referencing.
+ let encoder: XMLEncoderImplementation
+
+ /// The container reference itself.
+ private let reference: Reference
+
+ // MARK: - Initialization
+
+ /// Initializes `self` by referencing the given array container in the given encoder.
+ init(
+ referencing encoder: XMLEncoderImplementation,
+ at index: Int,
+ wrapping sharedUnkeyed: SharedBox
+ ) {
+ self.encoder = encoder
+ reference = .unkeyed(sharedUnkeyed, index)
+ super.init(
+ options: encoder.options,
+ nodeEncodings: encoder.nodeEncodings,
+ codingPath: encoder.codingPath
+ )
+
+ codingPath.append(XMLKey(index: index))
+ }
+
+ /// Initializes `self` by referencing the given dictionary container in the given encoder.
+ init(
+ referencing encoder: XMLEncoderImplementation,
+ key: CodingKey,
+ convertedKey: CodingKey,
+ wrapping sharedKeyed: SharedBox
+ ) {
+ self.encoder = encoder
+ reference = .keyed(sharedKeyed, convertedKey.stringValue)
+ super.init(
+ options: encoder.options,
+ nodeEncodings: encoder.nodeEncodings,
+ codingPath: encoder.codingPath
+ )
+
+ codingPath.append(key)
+ }
+
+ init(
+ referencing encoder: XMLEncoderImplementation,
+ key: CodingKey,
+ convertedKey: CodingKey,
+ wrapping sharedKeyed: SharedBox
+ ) {
+ self.encoder = encoder
+ reference = .choice(sharedKeyed, convertedKey.stringValue)
+ super.init(
+ options: encoder.options,
+ nodeEncodings: encoder.nodeEncodings,
+ codingPath: encoder.codingPath
+ )
+
+ codingPath.append(key)
+ }
+
+ // MARK: - Coding Path Operations
+
+ override var canEncodeNewValue: Bool {
+ // With a regular encoder, the storage and coding path grow together.
+ // A referencing encoder, however, inherits its parents coding path, as well as the key it was created for.
+ // We have to take this into account.
+ return storage.count == codingPath.count - encoder.codingPath.count - 1
+ }
+
+ // MARK: - Deinitialization
+
+ // Finalizes `self` by writing the contents of our storage to the referenced encoder's storage.
+ deinit {
+ let box: Box
+ switch self.storage.count {
+ case 0: box = KeyedBox()
+ case 1: box = self.storage.popContainer()
+ default: fatalError("Referencing encoder deallocated with multiple containers on stack.")
+ }
+
+ switch self.reference {
+ case let .unkeyed(sharedUnkeyedBox, index):
+ sharedUnkeyedBox.withShared { unkeyedBox in
+ unkeyedBox.insert(box, at: index)
+ }
+ case let .keyed(sharedKeyedBox, key):
+ sharedKeyedBox.withShared { keyedBox in
+ keyedBox.elements.append(box, at: key)
+ }
+ case let .choice(sharedChoiceBox, key):
+ sharedChoiceBox.withShared { choiceBox in
+ choiceBox.element = box
+ choiceBox.key = key
+ }
+ }
+ }
+}
diff --git a/third-party/XMLCoder/Sources/Encoder/XMLUnkeyedEncodingContainer.swift b/third-party/XMLCoder/Sources/Encoder/XMLUnkeyedEncodingContainer.swift
new file mode 100644
index 0000000000..87f72dc3c0
--- /dev/null
+++ b/third-party/XMLCoder/Sources/Encoder/XMLUnkeyedEncodingContainer.swift
@@ -0,0 +1,134 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by Vincent Esche on 11/20/18.
+//
+
+import Foundation
+
+struct XMLUnkeyedEncodingContainer: UnkeyedEncodingContainer {
+ // MARK: Properties
+
+ /// A reference to the encoder we're writing to.
+ private let encoder: XMLEncoderImplementation
+
+ /// A reference to the container we're writing to.
+ private let container: SharedBox
+
+ /// The path of coding keys taken to get to this point in encoding.
+ public private(set) var codingPath: [CodingKey]
+
+ /// The number of elements encoded into the container.
+ public var count: Int {
+ return container.withShared { $0.count }
+ }
+
+ // MARK: - Initialization
+
+ /// Initializes `self` with the given references.
+ init(
+ referencing encoder: XMLEncoderImplementation,
+ codingPath: [CodingKey],
+ wrapping container: SharedBox
+ ) {
+ self.encoder = encoder
+ self.codingPath = codingPath
+ self.container = container
+ }
+
+ // MARK: - UnkeyedEncodingContainer Methods
+
+ public mutating func encodeNil() throws {
+ container.withShared { container in
+ container.append(encoder.box())
+ }
+ }
+
+ public mutating func encode(_ value: T) throws {
+ try encode(value) { encoder, value in
+ try encoder.box(value)
+ }
+ }
+
+ private mutating func encode(
+ _ value: T,
+ encode: (XMLEncoderImplementation, T) throws -> Box
+ ) rethrows {
+ encoder.codingPath.append(XMLKey(index: count))
+ defer { self.encoder.codingPath.removeLast() }
+
+ try container.withShared { container in
+ container.append(try encode(encoder, value))
+ }
+ }
+
+ public mutating func nestedContainer(
+ keyedBy _: NestedKey.Type
+ ) -> KeyedEncodingContainer {
+ if NestedKey.self is XMLChoiceCodingKey.Type {
+ return nestedChoiceContainer(keyedBy: NestedKey.self)
+ } else {
+ return nestedKeyedContainer(keyedBy: NestedKey.self)
+ }
+ }
+
+ public mutating func nestedKeyedContainer(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer {
+ codingPath.append(XMLKey(index: count))
+ defer { self.codingPath.removeLast() }
+
+ let sharedKeyed = SharedBox(KeyedBox())
+ self.container.withShared { container in
+ container.append(sharedKeyed)
+ }
+
+ let container = XMLKeyedEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedKeyed
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ public mutating func nestedChoiceContainer(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer {
+ codingPath.append(XMLKey(index: count))
+ defer { self.codingPath.removeLast() }
+
+ let sharedChoice = SharedBox(ChoiceBox())
+ self.container.withShared { container in
+ container.append(sharedChoice)
+ }
+
+ let container = XMLChoiceEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedChoice
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
+ codingPath.append(XMLKey(index: count))
+ defer { self.codingPath.removeLast() }
+
+ let sharedUnkeyed = SharedBox(UnkeyedBox())
+ container.withShared { container in
+ container.append(sharedUnkeyed)
+ }
+
+ return XMLUnkeyedEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedUnkeyed
+ )
+ }
+
+ public mutating func superEncoder() -> Encoder {
+ return XMLReferencingEncoder(
+ referencing: encoder,
+ at: count,
+ wrapping: container
+ )
+ }
+}