Cyborg/VectorDrawableParser.swift (805 lines of code) (raw):
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreGraphics
import Foundation
import libxml2
import QuartzCore
/// Contains either an instance of `Wrapped`,
/// or an `error` if the instance could not be deserialized.
///
/// - note: XML errors are presented in <line:column:depth> format.
/// The line and column always correspond to the end of the relevant element.
public enum Result<Wrapped> {
case ok(Wrapped)
case error(ParseError)
func flatMap<T>(_ function: (Wrapped) -> Result<T>) -> Result<T> {
switch self {
case .ok(let wrapped): return function(wrapped)
case .error(let error): return .error(error)
}
}
}
extension Sequence {
func mapAllOrFail<T>(_ function: (Element) -> Result<T>) -> Result<[T]> {
var result = [T]()
result.reserveCapacity(underestimatedCount)
for element in self {
switch function(element) {
case .ok(let wrapped): result.append(wrapped)
case .error(let error): return .error(error)
}
}
return .ok(result)
}
}
/// An Error encountered when parsing. Generally optional, the presence of a value indicates that
/// an error occurred.
public typealias ParseError = String
public extension VectorDrawable {
/// Attempts to create a new `VectorDrawable` by opening the file in `url`.
///
/// - parameter url: The `URL` to load.
/// - returns: The `VectorDrawable`, or an error if parsing failed.
static func create(from url: URL) -> Result<VectorDrawable> {
do {
let data = try Data(contentsOf: url)
return create(from: data)
} catch let error {
return .error(error.localizedDescription)
}
}
/// Attempts to create a new `VectorDrawable` by parsing `data`.
///
/// - parameter data: The `Data` to parse.
/// - returns: The `VectorDrawable`, or an error if parsing failed.
static func create(from data: Data) -> Result<VectorDrawable> {
let parser = VectorParser()
return data.withBytes(or: .error("Empty data passed.")) { (bytes: UnsafePointer<Int8>) -> Result<VectorDrawable> in
let xml = xmlReaderForMemory(bytes,
Int32(data.count),
nil,
nil,
Int32(XML_PARSE_NOENT.rawValue))
var xmlError: UnsafeMutablePointer<CChar>?
let errorHandler: xmlTextReaderErrorFunc =
{ (xmlError: UnsafeMutableRawPointer?,
message: UnsafePointer<Int8>?,
severity: xmlParserSeverities,
location: xmlTextReaderLocatorPtr?) in
if severity == XML_PARSER_SEVERITY_ERROR {
if let message = message,
let xmlError = xmlError {
let lineNumber = xmlTextReaderLocatorLineNumber(location)
let error = """
<line number: \(lineNumber)>: \(String(cString: message))
""".utf8CString + [0]
error.withUnsafeBufferPointer { (buffer) in
if let address = buffer.baseAddress {
let e = UnsafeMutablePointer<CChar>.allocate(capacity: error.count)
e.assign(from: address, count: error.count)
xmlError.storeBytes(of: e, as: UnsafeMutablePointer<CChar>.self)
}
}
} else {
assertionFailure("There was an error, but a message or output variable wasn't provided.")
}
}
}
func xmlErrorOr(_ alternative: () -> (Result<VectorDrawable>)) -> Result<VectorDrawable> {
if let xmlError = xmlError {
defer {
xmlError
.deallocate()
}
return .error(String(cString: xmlError))
} else {
return alternative()
}
}
xmlTextReaderSetErrorHandler(xml,
errorHandler,
&xmlError)
defer {
xmlFreeTextReader(xml)
}
while xmlTextReaderRead(xml) == 1 {
let count = xmlTextReaderAttributeCount(xml)
if let namePointer = xmlTextReaderConstName(xml) {
let elementName = String(cString: namePointer)
let isEmpty = xmlTextReaderIsEmptyElement(xml) == 1
let type = xmlTextReaderNodeType(xml)
if type == XML_READER_TYPE_SIGNIFICANT_WHITESPACE.rawValue {
// we don't care about these, they show up as "#text"
// which disrupts the parsing
continue
}
if type == XML_READER_TYPE_END_ELEMENT.rawValue {
// The return value here indicates whether the parser ended, which we don't care about in this case.
_ = parser.didEnd(element: elementName)
continue
}
var attributes = [(XMLString, XMLString)]()
attributes.reserveCapacity(Int(count))
for _ in 0..<count {
if xmlTextReaderMoveToNextAttribute(xml) == 1 {
if let namePointer = xmlTextReaderConstName(xml),
let valuePointer = xmlTextReaderConstValue(xml) {
attributes.append((XMLString(namePointer),
XMLString(valuePointer)))
} else {
return xmlErrorOr {
"failed to parse attribute".withLocationInXML(xml)
}
}
} else {
return xmlErrorOr {
"failed to move to next attribute".withLocationInXML(xml)
}
}
}
if let parseError = parser.parse(element: elementName,
attributes: attributes) {
return parseError.withLocationInXML(xml)
}
if isEmpty {
// handle self closing tags (<tag ... />)
_ = parser.didEnd(element: elementName)
}
} else {
return xmlErrorOr {
"Failed to read element name".withLocationInXML(xml)
}
}
}
return xmlErrorOr {
parser.createElement()
}
}
}
}
fileprivate extension String {
func withLocationInXML(_ xml: xmlTextReaderPtr?) -> Result<VectorDrawable> {
let line = xmlTextReaderGetParserLineNumber(xml)
let column = xmlTextReaderGetParserColumnNumber(xml)
let depth = xmlTextReaderDepth(xml)
return .error("VectorDrawable Parsing Error: at XML <\(line):\(column):\(depth)> \n\(self)")
}
}
// MARK: - Element Parsers
fileprivate func assign<T>(_ string: XMLString,
to property: inout T,
creatingWith creator: (XMLString) -> (T?)) -> ParseError? {
if let value = creator(string) {
property = value
return nil
} else {
return "Could not assign \(string)"
}
}
fileprivate func assign<T>(_ string: XMLString,
to property: inout T?) -> ParseError? where T: XMLStringRepresentable {
assign(string, to: &property, creatingWith: T.init(_: ))
}
fileprivate func assign<T>(_ string: XMLString,
to property: inout T) -> ParseError? where T: XMLStringRepresentable {
assign(string, to: &property, creatingWith: T.init(_: ))
}
fileprivate func assignFloat(_ string: XMLString,
to path: inout CGFloat?) -> ParseError? {
assign(string, to: &path, creatingWith: CGFloat.init)
}
fileprivate func assignFloat(_ string: XMLString,
to path: inout CGFloat) -> ParseError? {
assign(string, to: &path, creatingWith: CGFloat.init)
}
protocol NodeParsing: AnyObject {
func parse(element: String, attributes: [(XMLString, XMLString)]) -> ParseError?
func didEnd(element: String) -> Bool
}
extension NodeParsing {
func parseAttributes<Enum>(_ attributes: [(XMLString, XMLString)],
_ onEachSuccess: (Enum, XMLString) -> (ParseError?)) -> ParseError? where Enum: XMLStringRepresentable {
for (key, value) in attributes {
if let error = convertToEnumOrFail(key, value, onEachSuccess) {
return error
}
}
return nil
}
fileprivate func convertToEnumOrFail<Enum>(_ attribute: XMLString,
_ value: XMLString,
_ onSuccess: (Enum, XMLString) -> (ParseError?)) -> ParseError? where Enum: XMLStringRepresentable {
if let result = Enum(attribute) {
return onSuccess(result, value)
} else {
return "\(attribute) is not a valid key for \(String(describing: type(of: self)))"
}
}
}
class ParentParser<Child>: NodeParsing where Child: NodeParsing {
var currentChild: Child?
var children: [Child] = []
var hasFoundElement = false
// Hack: indicates whether this is just a shell we created for the case where there's a child
// at a high level. For example a vector node with a path elemement as its child: we make a fake
// group for it so it type checks.
let isArtificial: Bool
init(isArtificial: Bool = false) {
self.isArtificial = isArtificial
}
var name: Element {
return .vector
}
func parse(element: String, attributes: [(XMLString, XMLString)]) -> ParseError? {
if let currentChild = currentChild {
return currentChild.parse(element: element, attributes: attributes)
} else if element == name.rawValue,
!hasFoundElement {
hasFoundElement = true
return parseAttributes(attributes)
} else if let (child, assignment) = childForElement(element) {
currentChild = child
assignment(child)
return child.parse(element: element, attributes: attributes)
} else {
if !hasFoundElement {
return "Element \"\(element)\" found, expected \(name.rawValue)."
} else {
return "Found element \"\(element)\" nested in \(type(of: self)), when it is not an acceptable child node."
}
}
}
func parseAttributes(_: [(XMLString, XMLString)]) -> ParseError? {
nil
}
func appendChild(_ child: Child) {
children.append(child)
}
func childForElement(_: String) -> (Child, (Child) -> ())? {
nil
}
func didEnd(element: String) -> Bool {
if let child = currentChild,
child.didEnd(element: element) {
currentChild = nil
return isArtificial
} else {
return element == name.rawValue
}
}
}
final class VectorParser: ParentParser<GroupParser> {
var baseWidth: CGFloat?
var baseHeight: CGFloat?
var viewPortWidth: CGFloat?
var viewPortHeight: CGFloat?
var tintMode: BlendMode?
var tintColor: Color?
var autoMirrored: Bool = false
var alpha: CGFloat = 1
override func parseAttributes(_ attributes: [(XMLString, XMLString)]) -> ParseError? {
var foundSchema = false
for (key, value) in attributes {
if let property = VectorProperty(rawValue: String(withoutCopying: key)) {
let result: ParseError?
switch property {
case .resourceSchema:
result = nil
case .schema:
foundSchema = true
result = nil
case .height: result = assign(value, to: &baseHeight, creatingWith: parseAndroidMeasurement(from:))
case .width: result = assign(value, to: &baseWidth, creatingWith: parseAndroidMeasurement(from:))
case .viewPortHeight: result = assignFloat(value, to: &viewPortHeight)
case .viewPortWidth: result = assignFloat(value, to: &viewPortWidth)
case .tint: result = assign(value, to: &tintColor, creatingWith: Color.init)
case .tintMode:
if let blendMode = BlendMode(value) {
tintMode = blendMode
result = nil
} else {
result = "Could not assign \(value)"
}
case .autoMirrored: result = assign(value, to: &autoMirrored, creatingWith: Bool.init)
case .alpha: result = assignFloat(value, to: &alpha)
}
if result != nil {
return result
}
} else {
return "Key \(key) is not a valid attribute of <vector>"
}
}
if foundSchema {
return nil
} else {
return "Schema not found in <vector>"
}
}
override func childForElement(_ element: String) -> (GroupParser, (GroupParser) -> ())? {
switch Element(rawValue: element) {
// The group parser already has all its elements filled out,
// so it'll "fall through" directly to the path.
// All we need to do is give it a name for it to complete.
case .some(.path): return (GroupParser(groupName: "anonymous", isArtificial: true), appendChild)
case .some(.group): return (GroupParser(), appendChild)
default: return nil
}
}
func createElement() -> Result<VectorDrawable> {
if let baseWidth = baseWidth,
let baseHeight = baseHeight,
let viewPortWidth = viewPortWidth,
let viewPortHeight = viewPortHeight {
return children.mapAllOrFail { group in
group.createElement(in: CGSize(width: viewPortWidth, height: viewPortHeight))
}
.flatMap { (groups) -> Result<VectorDrawable> in
.ok(.init(baseWidth: baseWidth,
baseHeight: baseHeight,
viewPortWidth: viewPortWidth,
viewPortHeight: viewPortHeight,
baseAlpha: alpha,
groups: groups,
autoMirrored: autoMirrored))
}
} else {
return .error("Could not parse a <vector> element, but there was no error. This is a bug in the VectorDrawable Library.")
}
}
func parseAndroidMeasurement(from text: XMLString) -> CGFloat? {
if case .ok(let number, let index) = number(from: text, at: 0),
let _ = AndroidUnitOfMeasure(rawValue: String(withoutCopying: text[index..<text.count])) {
return number
} else {
return nil
}
}
}
final class PathParser: ParentParser<GradientParser>, GroupChildParser {
static let name: Element = .path
override var name: Element {
.path
}
var pathName: String?
var commands: ContiguousArray<PathSegment>?
var fillColor: Color?
var strokeColor: Color?
var strokeWidth: CGFloat = 0
var strokeAlpha: CGFloat = 1
var fillAlpha: CGFloat = 1
var trimPathStart: CGFloat = 0
var trimPathEnd: CGFloat = 1
var trimPathOffset: CGFloat = 0
var strokeLineCap: LineCap = .butt
var strokeMiterLimit: CGFloat = 4
var strokeLineJoin: LineJoin = .miter
var fillType: CAShapeLayerFillRule = .nonZero
override func parseAttributes(_ attributes: [(XMLString, XMLString)]) -> ParseError? {
let baseError = "Error parsing the <android:pathData> tag: "
for (key, value) in attributes {
if let property = PathProperty(rawValue: String(withoutCopying: key)) {
let result: ParseError?
switch property {
case .name:
pathName = String(withoutCopying: value)
result = nil
case .pathData:
let subResult: ParseError?
let parsers = allDrawingCommands
switch consumeAll(using: parsers)(value, 0) {
case .ok(let result, _):
commands = result
subResult = nil
case .error(let error):
subResult = baseError + error.message
}
result = subResult
case .fillColor:
if let color = Color(value) {
fillColor = color
result = nil
} else {
result = "Failed to create a color from \"\(String(copying: value))\""
}
case .strokeWidth:
result = assignFloat(value, to: &strokeWidth)
case .strokeColor:
result = assign(value, to: &strokeColor, creatingWith: Color.init)
case .strokeAlpha:
result = assignFloat(value, to: &strokeAlpha)
case .fillAlpha:
result = assignFloat(value, to: &fillAlpha)
case .trimPathStart:
result = assignFloat(value, to: &trimPathStart)
case .trimPathEnd:
result = assignFloat(value, to: &trimPathEnd)
case .trimPathOffset:
result = assignFloat(value, to: &trimPathOffset)
case .strokeLineCap:
result = assign(value, to: &strokeLineCap, creatingWith: LineCap.init)
case .strokeLineJoin:
result = assign(value, to: &strokeLineJoin, creatingWith: LineJoin.init)
case .strokeMiterLimit:
result = assignFloat(value, to: &strokeMiterLimit)
case .fillType:
result = assign(value, to: &fillType, creatingWith: { (string) -> (CAShapeLayerFillRule?) in
switch string {
case "evenOdd": return .evenOdd
case "nonZero": return .nonZero
default: return nil
}
})
}
if result != nil {
return result
}
} else {
return "Key \(key) is not a valid attribute of <path>."
}
}
return nil
}
override func didEnd(element: String) -> Bool {
if let currentChild = currentChild {
return currentChild.didEnd(element: element)
} else {
return element == Element.path.rawValue
}
}
override func childForElement(_ element: String) -> (GradientParser, (GradientParser) -> ())? {
if element == "aapt:attr" {
return (GradientParser(), appendChild)
} else {
return nil
}
}
func createElement(in viewportSize: CGSize) -> Result<GroupChild> {
let gradient: VectorDrawable.Gradient?
if let first = children.first,
let definiteGradient = first.createElement(in: viewportSize) {
gradient = definiteGradient
} else {
gradient = nil
}
if let commands = commands {
return .ok(VectorDrawable.Path(name: pathName,
fillColor: fillColor,
fillAlpha: fillAlpha,
data: Array(commands.joined()),
strokeColor: strokeColor,
strokeWidth: strokeWidth,
strokeAlpha: strokeAlpha,
trimPathStart: trimPathStart,
trimPathEnd: trimPathEnd,
trimPathOffset: trimPathOffset,
strokeLineCap: strokeLineCap,
strokeLineJoin: strokeLineJoin,
fillType: fillType,
gradient: gradient))
} else {
return .error("\(PathProperty.pathData.rawValue) is a required property of <\(PathParser.name.rawValue)>.")
}
}
}
protocol GroupChildParser: NodeParsing {
func createElement(in viewportSize: CGSize) -> Result<GroupChild>
}
final class ClipPathParser: NodeParsing, GroupChildParser {
func createElement(in viewportSize: CGSize) -> Result<GroupChild> {
switch (createElement as () -> (Result<VectorDrawable.ClipPath>))() {
case .ok(let element): return .ok(element)
case .error(let error): return .error(error)
}
}
var name: String?
var commands: ContiguousArray<PathSegment>?
var fillType: CAShapeLayerFillRule?
func parse(element _: String, attributes: [(XMLString, XMLString)]) -> ParseError? {
for (key, value) in attributes {
if let property = ClipPathProperty(rawValue: String(withoutCopying: key)) {
switch property {
case .name: name = String(withoutCopying: value)
case .pathData:
let parsers = allDrawingCommands
switch consumeAll(using: parsers)(value, 0) {
case .ok(let result, _):
commands = result
case .error(let error):
let baseError = "Error parsing the <android:clipPath> tag: "
return baseError + error.message
}
case .fillType:
fillType = CAShapeLayerFillRule(rawValue: String(copying: value))
case .fillColor, .strokeColor:
// TODO: determine whether this needs to be handled
break
}
} else {
return "Key \"\(key)\" is not a valid property of ClipPath."
}
}
return nil
}
func didEnd(element _: String) -> Bool {
true
}
func createElement() -> Result<VectorDrawable.ClipPath> {
if let commands = commands {
return .ok(.init(name: name,
path: Array(commands.joined()),
fillType: fillType ?? .evenOdd))
} else {
return .error("Didn't find \(PathProperty.pathData.rawValue), which is required in elements of type <\(Element.clipPath.rawValue)>")
}
}
}
/// Necessary because we can't use a protocol to satisfy a generic with
/// type bounds, as that would make it impossible to dispatch static functions.
final class AnyGroupParserChild: GroupChildParser {
let parser: GroupChildParser
init(erasing parser: GroupChildParser) {
self.parser = parser
}
func parse(element: String, attributes: [(XMLString, XMLString)]) -> ParseError? {
parser.parse(element: element, attributes: attributes)
}
func didEnd(element: String) -> Bool {
parser.didEnd(element: element)
}
func createElement(in viewportSize: CGSize) -> Result<GroupChild> {
parser.createElement(in: viewportSize)
}
}
final class GroupParser: ParentParser<AnyGroupParserChild>, GroupChildParser {
override var name: Element {
return .group
}
var groupName: String?
var pivotX: CGFloat = 0
var pivotY: CGFloat = 0
var rotation: CGFloat = 0
var scaleX: CGFloat = 1
var scaleY: CGFloat = 1
var translationX: CGFloat = 0
var translationY: CGFloat = 0
var clipPaths = [ClipPathParser]()
init(groupName: String? = nil, isArtificial: Bool = false) {
self.groupName = groupName
super.init(isArtificial: isArtificial)
}
override func parseAttributes(_ attributes: [(XMLString, XMLString)]) -> ParseError? {
for (key, value) in attributes {
if let property = GroupProperty(rawValue: String(withoutCopying: key)) {
let result: ParseError?
switch property {
case .name:
groupName = String(withoutCopying: value)
result = nil
case .rotation:
result = assignFloat(value, to: &rotation)
case .pivotX:
result = assignFloat(value, to: &pivotX)
case .pivotY:
result = assignFloat(value, to: &pivotY)
case .scaleX:
result = assignFloat(value, to: &scaleX)
case .scaleY:
result = assignFloat(value, to: &scaleY)
case .translateX:
result = assignFloat(value, to: &translationX)
case .translateY:
result = assignFloat(value, to: &translationY)
}
if result != nil {
return result
}
} else {
return "Unrecognized Attribute: \(key)"
}
}
return nil
}
func createElement(in viewportSize: CGSize) -> Result<GroupChild> {
children.mapAllOrFail { parser in
parser.createElement(in: viewportSize)
}
.flatMap { childElements in
clipPaths
.mapAllOrFail { $0.createElement() as Result<VectorDrawable.ClipPath> }
.flatMap { clipPaths in
.ok(VectorDrawable.Group(name: groupName,
transform: Transform(pivot: .init(x: pivotX, y: pivotY),
rotation: rotation,
scale: .init(x: scaleX, y: scaleY),
translation: .init(x: translationX, y: translationY)),
children: childElements,
clipPaths: clipPaths))
}
}
}
override func childForElement(_ element: String) -> (AnyGroupParserChild, (AnyGroupParserChild) -> ())? {
switch Element(rawValue: element) {
case .some(.path): return (AnyGroupParserChild(erasing: PathParser()), appendChild)
case .some(.group): return (AnyGroupParserChild(erasing: GroupParser()), appendChild)
case .some(.clipPath):
let parser = ClipPathParser()
return (AnyGroupParserChild(erasing: parser), { _ in self.clipPaths.append(parser) })
default: return nil
}
}
}
class GradientParser: NodeParsing {
let androidResourceAttribute = "aapt:attr"
var startX: CGFloat?
var startY: CGFloat?
var endX: CGFloat?
var endY: CGFloat?
var centerX: CGFloat?
var centerY: CGFloat?
var radius: CGFloat?
var type: GradientType = .linear
var offsets: [VectorDrawable.Gradient.Offset] = []
var centerColor: Color?
var startColor: Color?
var endColor: Color?
var tileMode: TileMode = .clamp
func parse(element: String, attributes: [(XMLString, XMLString)]) -> ParseError? {
if element == androidResourceAttribute {
for (key, value) in attributes {
if String(withoutCopying: key) != "name" || String(withoutCopying: value) != PathProperty.fillColor.rawValue {
return "Only \(PathProperty.fillColor.rawValue) is supported in \(androidResourceAttribute) elements."
} else {
return nil
}
}
return nil
} else if element == "gradient" {
return parseAttributes(attributes) { (property: GradientProperty, value) -> (ParseError?) in
switch property {
case .centerColor: return assign(value, to: ¢erColor, creatingWith: Color.init)
case .startY: return assignFloat(value, to: &startY)
case .startX: return assignFloat(value, to: &startX)
case .endY: return assignFloat(value, to: &endY)
case .endX: return assignFloat(value, to: &endX)
case .type: return assign(value, to: &type)
case .startcolor: return assign(value, to: &startColor, creatingWith: Color.init)
case .endColor: return assign(value, to: &endColor, creatingWith: Color.init)
case .tileMode: return assign(value, to: &tileMode)
case .centerX: return assignFloat(value, to: ¢erX)
case .centerY: return assignFloat(value, to: ¢erY)
case .gradientRadius: return assignFloat(value, to: &radius)
}
}
} else if element == "item" {
var offset: CGFloat?
var color: Color?
let error = parseAttributes(attributes) { (property: ItemProperty, value) -> (ParseError?) in
switch property {
case .offset: return assignFloat(value, to: &offset)
case .color: return assign(value, to: &color, creatingWith: Color.init)
}
}
if let error = error {
return error
} else {
switch (color, offset) {
case (.some(let color), .some(let offset)):
offsets.append(VectorDrawable.Gradient.Offset(amount: offset, color: color))
return nil
case (.none, .some):
return "Missing color"
case (.some, .none):
return "Missing offset"
case (.none, .none):
return "Missing color and offset"
}
}
} else {
return "Invalid element \"\(element)\""
}
}
func didEnd(element: String) -> Bool {
element == androidResourceAttribute
}
func createElement(in viewportSize: CGSize) -> VectorDrawable.Gradient? {
switch type {
case .linear:
if let startX = startX,
let startY = startY,
let endX = endX,
let endY = endY {
let startXUnit = startX / viewportSize.width
let startYUnit = startY / viewportSize.height
let endXUnit = endX / viewportSize.width
let endYUnit = endY / viewportSize.height
return VectorDrawable.LinearGradient(startColor: startColor,
centerColor: centerColor,
endColor: endColor,
tileMode: tileMode,
startX: startXUnit,
startY: startYUnit,
endX: endXUnit,
endY: endYUnit,
offsets: offsets)
} else {
return nil
}
case .radial:
if let centerX = centerX,
let centerY = centerY,
let radius = radius {
return VectorDrawable.RadialGradient(startColor: startColor,
centerColor: centerColor,
endColor: endColor,
tileMode: tileMode,
centerX: centerX,
centerY: centerY,
radius: radius,
offsets: offsets)
} else {
return nil
}
case .sweep:
if let centerX = centerX,
let centerY = centerY {
return VectorDrawable.SweepGradient(startColor: startColor,
centerColor: centerColor,
endColor: endColor,
tileMode: tileMode,
centerX: centerX,
centerY: centerY,
offsets: offsets)
} else {
return nil
}
}
}
}
// MARK: - Parser Combinators
func number(from stream: XMLString, at index: Int32) -> ParseResult<CGFloat> {
let substring = stream[index..<stream.count]
return substring
.withSignedIntegers { buffer in
var next: UnsafeMutablePointer<Int8>? = UnsafeMutablePointer(mutating: buffer)
let result = strtod(buffer, &next)
if result == 0.0,
next == buffer {
return .error(.failedToParseNumber(.init(index: index, stream: stream)))
} else if var final = next {
if final.pointee == .comma {
final = final.advanced(by: 1)
}
let index = index + Int32(buffer.distance(to: final))
return .ok(CGFloat(result), index)
} else {
return .error(.failedToParseNumber(.init(index: index, stream: stream)))
}
}
}
func numbers() -> Parser<[CGFloat]> {
return { stream, index in
var result = [CGFloat]()
var nextIndex = index
while case .ok(let value, let index) = number(from: stream, at: nextIndex) {
result.append(value)
nextIndex = index
}
if result.count > 0 {
return .ok(result, nextIndex)
} else {
return .error(.failedToParseNumber(.init(index: nextIndex, stream: stream)))
}
}
}
func coordinatePair() -> Parser<CGPoint> {
return { stream, index in
var point: CGPoint = .zero
var next = index
if case .ok(let found, let index) = number(from: stream, at: next) {
point.x = CGFloat(found)
next = index
} else {
return .error(.noFirstMemberInCoordinatePair(.init(index: next, stream: stream)))
}
if case .ok(let found, let index) = number(from: stream, at: next) {
point.y = CGFloat(found)
next = index
} else {
return .error(.noSecondMemberInCoordinatePair(.init(index: next, stream: stream)))
}
return .ok(point, next)
}
}
extension Data {
#if compiler(>=5.0)
func withBytes<T, U>(or alternative: T, _ function: (UnsafePointer<U>) -> (T)) -> T {
return withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> T in
if let baseAddress = pointer.baseAddress, pointer.count != 0 {
let bytes = baseAddress.assumingMemoryBound(to: U.self)
return function(bytes)
} else {
return alternative
}
}
}
#else
func withBytes<T, U>(or alternative: T, _ function: (UnsafePointer<U>) -> (T)) -> T {
return withUnsafeBytes { (pointer: UnsafePointer<U>) -> T in
if count == 0 {
return alternative
} else {
return function(pointer)
}
}
}
#endif
}