From e51fef57d8946c13ddae1120be8610aefa563c45 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Fri, 24 Apr 2026 13:09:33 +0400 Subject: [PATCH] Add swift svg --- submodules/PhotoResources/BUILD | 2 +- .../Sources/PhotoResources.swift | 2 +- submodules/Svg/BUILD | 19 +- submodules/Svg/LegacyImpl/BUILD | 23 + .../{ => LegacyImpl}/PublicHeaders/Svg/Svg.h | 2 +- submodules/Svg/{ => LegacyImpl}/Sources/Svg.m | 2 +- .../Svg/{ => LegacyImpl}/Sources/nanosvg.c | 0 .../Svg/{ => LegacyImpl}/Sources/nanosvg.h | 0 submodules/Svg/Sources/Svg.swift | 7 + submodules/TelegramUI/BUILD | 2 +- .../Sources/PeerInfoRatingComponent.swift | 4 +- .../Sources/FetchCachedRepresentations.swift | 2 +- submodules/WallpaperResources/BUILD | 2 +- .../Sources/WallpaperResources.swift | 4 +- third-party/Swift2D/BUILD | 17 + .../Swift2D/Sources/Point+CoreGraphics.swift | 19 + third-party/Swift2D/Sources/Point.swift | 82 +++ .../Swift2D/Sources/Rect+CoreGraphics.swift | 32 ++ third-party/Swift2D/Sources/Rect.swift | 251 +++++++++ .../Swift2D/Sources/Size+CoreGraphics.swift | 19 + third-party/Swift2D/Sources/Size.swift | 87 +++ third-party/SwiftColor/BUILD | 17 + third-party/SwiftColor/Sources/Clamping.swift | 20 + .../SwiftColor/Sources/ColorSpace.swift | 3 + .../SwiftColor/Sources/Pigment+AppKit.swift | 24 + .../Sources/Pigment+CoreGraphics.swift | 39 ++ .../SwiftColor/Sources/Pigment+Float.swift | 95 ++++ .../SwiftColor/Sources/Pigment+Hex.swift | 145 +++++ .../SwiftColor/Sources/Pigment+Int.swift | 89 ++++ .../SwiftColor/Sources/Pigment+Name.swift | 470 +++++++++++++++++ .../SwiftColor/Sources/Pigment+String.swift | 76 +++ .../SwiftColor/Sources/Pigment+SwiftUI.swift | 9 + .../SwiftColor/Sources/Pigment+UIKit.swift | 37 ++ third-party/SwiftColor/Sources/Pigment.swift | 53 ++ third-party/SwiftSVG/BUILD | 19 + third-party/SwiftSVG/Sources/Circle.swift | 93 ++++ .../Sources/CommandRepresentable.swift | 15 + third-party/SwiftSVG/Sources/Container.swift | 58 ++ .../SwiftSVG/Sources/CoreAttributes.swift | 17 + third-party/SwiftSVG/Sources/Element.swift | 46 ++ third-party/SwiftSVG/Sources/Ellipse.swift | 93 ++++ .../Sources/Extensions/Point+SwiftSVG.swift | 37 ++ third-party/SwiftSVG/Sources/Fill.swift | 44 ++ third-party/SwiftSVG/Sources/Group.swift | 152 ++++++ .../Sources/Internal/EllipseProcessor.swift | 88 ++++ .../Sources/Internal/PathProcessor.swift | 12 + .../Sources/Internal/PolygonProcressor.swift | 52 ++ .../Sources/Internal/PolylineProcessor.swift | 54 ++ .../Sources/Internal/RectangleProcessor.swift | 175 ++++++ third-party/SwiftSVG/Sources/Line.swift | 98 ++++ .../SwiftSVG/Sources/Path.Command.swift | 276 ++++++++++ .../SwiftSVG/Sources/Path.Component.swift | 98 ++++ .../Sources/Path.ComponentParser.swift | 275 ++++++++++ third-party/SwiftSVG/Sources/Path.swift | 101 ++++ third-party/SwiftSVG/Sources/Polygon.swift | 82 +++ third-party/SwiftSVG/Sources/Polyline.swift | 81 +++ .../Sources/PresentationAttributes.swift | 120 +++++ third-party/SwiftSVG/Sources/Rectangle.swift | 115 ++++ .../SwiftSVG/Sources/SVG+Swift2D.swift | 62 +++ third-party/SwiftSVG/Sources/SVG.swift | 160 ++++++ third-party/SwiftSVG/Sources/Stroke.swift | 64 +++ .../SwiftSVG/Sources/StylingAttributes.swift | 17 + third-party/SwiftSVG/Sources/Text.swift | 110 ++++ .../SwiftSVG/Sources/Transformation.swift | 113 ++++ third-party/VectorPlus/BUILD | 19 + .../CoreGraphics/CGContext+Render.swift | 72 +++ .../CGMutablePath+Instruction.swift | 45 ++ .../CoreGraphics/Fill+CoreGraphics.swift | 14 + .../Sources/CoreGraphics/SVG+CGPath.swift | 37 ++ .../CoreGraphics/Stroke+CoreGraphics.swift | 25 + .../VectorPlus/Sources/Fill+VectorPlus.swift | 29 + .../VectorPlus/Sources/Path+VectorPlus.swift | 34 ++ .../Sources/Path.Command+VectorPlus.swift | 216 ++++++++ .../Sources/Pigment+VectorPlus.swift | 7 + .../VectorPlus/Sources/Point+VectorPlus.swift | 7 + .../VectorPlus/Sources/Rect+VectorPlus.swift | 7 + .../VectorPlus/Sources/SVG+AppleSymbols.swift | 324 ++++++++++++ .../VectorPlus/Sources/SVG+Template.swift | 56 ++ .../VectorPlus/Sources/Size+VectorPlus.swift | 7 + .../Sources/Stroke+VectorPlus.swift | 40 ++ .../Sources/Template+UIImageView.swift | 177 +++++++ .../Sources/UIKit/SVG+UIImage.swift | 43 ++ .../Sources/UIKit/SVGImageView.swift | 81 +++ .../VectorPlus/Sources/VectorPoint.swift | 126 +++++ third-party/XMLCoder/BUILD | 17 + .../Sources/Auxiliaries/Attribute.swift | 99 ++++ .../Sources/Auxiliaries/Box/BoolBox.swift | 53 ++ .../Sources/Auxiliaries/Box/Box.swift | 32 ++ .../Sources/Auxiliaries/Box/ChoiceBox.swift | 41 ++ .../Sources/Auxiliaries/Box/DataBox.swift | 57 ++ .../Sources/Auxiliaries/Box/DateBox.swift | 99 ++++ .../Sources/Auxiliaries/Box/DecimalBox.swift | 62 +++ .../Sources/Auxiliaries/Box/DoubleBox.swift | 49 ++ .../Sources/Auxiliaries/Box/FloatBox.swift | 78 +++ .../Sources/Auxiliaries/Box/IntBox.swift | 59 +++ .../Sources/Auxiliaries/Box/KeyedBox.swift | 57 ++ .../Sources/Auxiliaries/Box/NullBox.swift | 33 ++ .../Sources/Auxiliaries/Box/SharedBox.swift | 35 ++ .../Auxiliaries/Box/SingleKeyedBox.swift | 25 + .../Sources/Auxiliaries/Box/StringBox.swift | 39 ++ .../Sources/Auxiliaries/Box/UIntBox.swift | 62 +++ .../Sources/Auxiliaries/Box/URLBox.swift | 44 ++ .../Sources/Auxiliaries/Box/UnkeyedBox.swift | 19 + .../Sources/Auxiliaries/Box/ValueBox.swift | 13 + .../Sources/Auxiliaries/Element.swift | 43 ++ .../Auxiliaries/ElementAndAttribute.swift | 44 ++ .../Auxiliaries/ISO8601DateFormatter.swift | 17 + .../Sources/Auxiliaries/KeyedStorage.swift | 82 +++ .../Sources/Auxiliaries/Metatypes.swift | 31 ++ .../Auxiliaries/String+Extensions.swift | 52 ++ .../XMLCoder/Sources/Auxiliaries/Utils.swift | 13 + .../Auxiliaries/XMLChoiceCodingKey.swift | 59 +++ .../Sources/Auxiliaries/XMLCoderElement.swift | 451 ++++++++++++++++ .../Sources/Auxiliaries/XMLDocumentType.swift | 65 +++ .../Sources/Auxiliaries/XMLHeader.swift | 57 ++ .../XMLCoder/Sources/Auxiliaries/XMLKey.swift | 38 ++ .../Sources/Auxiliaries/XMLStackParser.swift | 200 +++++++ .../Decoder/DecodingErrorExtension.swift | 54 ++ .../Sources/Decoder/DynamicNodeDecoding.swift | 44 ++ .../SingleValueDecodingContainer.swift | 57 ++ .../Decoder/XMLChoiceDecodingContainer.swift | 105 ++++ .../XMLCoder/Sources/Decoder/XMLDecoder.swift | 405 ++++++++++++++ .../Decoder/XMLDecoderImplementation.swift | 497 ++++++++++++++++++ .../Sources/Decoder/XMLDecodingStorage.swift | 52 ++ .../Decoder/XMLKeyedDecodingContainer.swift | 417 +++++++++++++++ .../Decoder/XMLUnkeyedDecodingContainer.swift | 232 ++++++++ .../Sources/Encoder/DynamicNodeEncoding.swift | 57 ++ .../Encoder/EncodingErrorExtension.swift | 36 ++ .../SingleValueEncodingContainer.swift | 103 ++++ .../Encoder/XMLChoiceEncodingContainer.swift | 208 ++++++++ .../XMLCoder/Sources/Encoder/XMLEncoder.swift | 451 ++++++++++++++++ .../Encoder/XMLEncoderImplementation.swift | 292 ++++++++++ .../Sources/Encoder/XMLEncodingStorage.swift | 66 +++ .../Encoder/XMLKeyedEncodingContainer.swift | 267 ++++++++++ .../Encoder/XMLReferencingEncoder.swift | 130 +++++ .../Encoder/XMLUnkeyedEncodingContainer.swift | 134 +++++ 136 files changed, 11457 insertions(+), 23 deletions(-) create mode 100644 submodules/Svg/LegacyImpl/BUILD rename submodules/Svg/{ => LegacyImpl}/PublicHeaders/Svg/Svg.h (84%) rename submodules/Svg/{ => LegacyImpl}/Sources/Svg.m (99%) rename submodules/Svg/{ => LegacyImpl}/Sources/nanosvg.c (100%) rename submodules/Svg/{ => LegacyImpl}/Sources/nanosvg.h (100%) create mode 100644 submodules/Svg/Sources/Svg.swift create mode 100644 third-party/Swift2D/BUILD create mode 100644 third-party/Swift2D/Sources/Point+CoreGraphics.swift create mode 100644 third-party/Swift2D/Sources/Point.swift create mode 100644 third-party/Swift2D/Sources/Rect+CoreGraphics.swift create mode 100644 third-party/Swift2D/Sources/Rect.swift create mode 100644 third-party/Swift2D/Sources/Size+CoreGraphics.swift create mode 100644 third-party/Swift2D/Sources/Size.swift create mode 100644 third-party/SwiftColor/BUILD create mode 100644 third-party/SwiftColor/Sources/Clamping.swift create mode 100644 third-party/SwiftColor/Sources/ColorSpace.swift create mode 100644 third-party/SwiftColor/Sources/Pigment+AppKit.swift create mode 100644 third-party/SwiftColor/Sources/Pigment+CoreGraphics.swift create mode 100644 third-party/SwiftColor/Sources/Pigment+Float.swift create mode 100644 third-party/SwiftColor/Sources/Pigment+Hex.swift create mode 100644 third-party/SwiftColor/Sources/Pigment+Int.swift create mode 100644 third-party/SwiftColor/Sources/Pigment+Name.swift create mode 100644 third-party/SwiftColor/Sources/Pigment+String.swift create mode 100644 third-party/SwiftColor/Sources/Pigment+SwiftUI.swift create mode 100644 third-party/SwiftColor/Sources/Pigment+UIKit.swift create mode 100644 third-party/SwiftColor/Sources/Pigment.swift create mode 100644 third-party/SwiftSVG/BUILD create mode 100644 third-party/SwiftSVG/Sources/Circle.swift create mode 100644 third-party/SwiftSVG/Sources/CommandRepresentable.swift create mode 100644 third-party/SwiftSVG/Sources/Container.swift create mode 100644 third-party/SwiftSVG/Sources/CoreAttributes.swift create mode 100644 third-party/SwiftSVG/Sources/Element.swift create mode 100644 third-party/SwiftSVG/Sources/Ellipse.swift create mode 100644 third-party/SwiftSVG/Sources/Extensions/Point+SwiftSVG.swift create mode 100644 third-party/SwiftSVG/Sources/Fill.swift create mode 100644 third-party/SwiftSVG/Sources/Group.swift create mode 100644 third-party/SwiftSVG/Sources/Internal/EllipseProcessor.swift create mode 100644 third-party/SwiftSVG/Sources/Internal/PathProcessor.swift create mode 100644 third-party/SwiftSVG/Sources/Internal/PolygonProcressor.swift create mode 100644 third-party/SwiftSVG/Sources/Internal/PolylineProcessor.swift create mode 100644 third-party/SwiftSVG/Sources/Internal/RectangleProcessor.swift create mode 100644 third-party/SwiftSVG/Sources/Line.swift create mode 100644 third-party/SwiftSVG/Sources/Path.Command.swift create mode 100644 third-party/SwiftSVG/Sources/Path.Component.swift create mode 100644 third-party/SwiftSVG/Sources/Path.ComponentParser.swift create mode 100644 third-party/SwiftSVG/Sources/Path.swift create mode 100644 third-party/SwiftSVG/Sources/Polygon.swift create mode 100644 third-party/SwiftSVG/Sources/Polyline.swift create mode 100644 third-party/SwiftSVG/Sources/PresentationAttributes.swift create mode 100644 third-party/SwiftSVG/Sources/Rectangle.swift create mode 100644 third-party/SwiftSVG/Sources/SVG+Swift2D.swift create mode 100644 third-party/SwiftSVG/Sources/SVG.swift create mode 100644 third-party/SwiftSVG/Sources/Stroke.swift create mode 100644 third-party/SwiftSVG/Sources/StylingAttributes.swift create mode 100644 third-party/SwiftSVG/Sources/Text.swift create mode 100644 third-party/SwiftSVG/Sources/Transformation.swift create mode 100644 third-party/VectorPlus/BUILD create mode 100644 third-party/VectorPlus/Sources/CoreGraphics/CGContext+Render.swift create mode 100644 third-party/VectorPlus/Sources/CoreGraphics/CGMutablePath+Instruction.swift create mode 100644 third-party/VectorPlus/Sources/CoreGraphics/Fill+CoreGraphics.swift create mode 100644 third-party/VectorPlus/Sources/CoreGraphics/SVG+CGPath.swift create mode 100644 third-party/VectorPlus/Sources/CoreGraphics/Stroke+CoreGraphics.swift create mode 100644 third-party/VectorPlus/Sources/Fill+VectorPlus.swift create mode 100644 third-party/VectorPlus/Sources/Path+VectorPlus.swift create mode 100644 third-party/VectorPlus/Sources/Path.Command+VectorPlus.swift create mode 100644 third-party/VectorPlus/Sources/Pigment+VectorPlus.swift create mode 100644 third-party/VectorPlus/Sources/Point+VectorPlus.swift create mode 100644 third-party/VectorPlus/Sources/Rect+VectorPlus.swift create mode 100644 third-party/VectorPlus/Sources/SVG+AppleSymbols.swift create mode 100644 third-party/VectorPlus/Sources/SVG+Template.swift create mode 100644 third-party/VectorPlus/Sources/Size+VectorPlus.swift create mode 100644 third-party/VectorPlus/Sources/Stroke+VectorPlus.swift create mode 100644 third-party/VectorPlus/Sources/Template+UIImageView.swift create mode 100644 third-party/VectorPlus/Sources/UIKit/SVG+UIImage.swift create mode 100644 third-party/VectorPlus/Sources/UIKit/SVGImageView.swift create mode 100644 third-party/VectorPlus/Sources/VectorPoint.swift create mode 100644 third-party/XMLCoder/BUILD create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Attribute.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/BoolBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/Box.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/ChoiceBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/DataBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/DateBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/DecimalBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/DoubleBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/FloatBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/IntBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/KeyedBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/NullBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/SharedBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/SingleKeyedBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/StringBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/UIntBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/URLBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/UnkeyedBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Box/ValueBox.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Element.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/ElementAndAttribute.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/ISO8601DateFormatter.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/KeyedStorage.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Metatypes.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/String+Extensions.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/Utils.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/XMLChoiceCodingKey.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/XMLCoderElement.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/XMLDocumentType.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/XMLHeader.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/XMLKey.swift create mode 100644 third-party/XMLCoder/Sources/Auxiliaries/XMLStackParser.swift create mode 100644 third-party/XMLCoder/Sources/Decoder/DecodingErrorExtension.swift create mode 100644 third-party/XMLCoder/Sources/Decoder/DynamicNodeDecoding.swift create mode 100644 third-party/XMLCoder/Sources/Decoder/SingleValueDecodingContainer.swift create mode 100644 third-party/XMLCoder/Sources/Decoder/XMLChoiceDecodingContainer.swift create mode 100644 third-party/XMLCoder/Sources/Decoder/XMLDecoder.swift create mode 100644 third-party/XMLCoder/Sources/Decoder/XMLDecoderImplementation.swift create mode 100644 third-party/XMLCoder/Sources/Decoder/XMLDecodingStorage.swift create mode 100644 third-party/XMLCoder/Sources/Decoder/XMLKeyedDecodingContainer.swift create mode 100644 third-party/XMLCoder/Sources/Decoder/XMLUnkeyedDecodingContainer.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/DynamicNodeEncoding.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/EncodingErrorExtension.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/SingleValueEncodingContainer.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/XMLChoiceEncodingContainer.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/XMLEncoder.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/XMLEncoderImplementation.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/XMLEncodingStorage.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/XMLKeyedEncodingContainer.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/XMLReferencingEncoder.swift create mode 100644 third-party/XMLCoder/Sources/Encoder/XMLUnkeyedEncodingContainer.swift 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 "\(contents)\n" + } +} + +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 += "" + } + } 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 + ) + } +}