diff --git a/Macaw.xcodeproj/project.pbxproj b/Macaw.xcodeproj/project.pbxproj index 10d2385e..84cd4c12 100644 --- a/Macaw.xcodeproj/project.pbxproj +++ b/Macaw.xcodeproj/project.pbxproj @@ -233,7 +233,6 @@ 57614B661F83D15600875933 /* PinchEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E10C1E3B393900D1CB28 /* PinchEvent.swift */; }; 57614B671F83D15600875933 /* ContentsInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57A27BCE1E44C4EC0057BD3A /* ContentsInterpolation.swift */; }; 57614B681F83D15600875933 /* GroupRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E13E1E3B393900D1CB28 /* GroupRenderer.swift */; }; - 57614B691F83D15600875933 /* SVGParserRegexHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E1491E3B393900D1CB28 /* SVGParserRegexHelper.swift */; }; 57614B6B1F83D15600875933 /* NSTimer+Closure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E14E1E3B393900D1CB28 /* NSTimer+Closure.swift */; }; 57614B6C1F83D15600875933 /* SWXMLHash+TypeConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 572CEFC51E2CED4B008C7C83 /* SWXMLHash+TypeConversion.swift */; }; 57614B6D1F83D15600875933 /* AnimationSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E0FF1E3B393900D1CB28 /* AnimationSequence.swift */; }; @@ -330,7 +329,6 @@ 57E5E1AA1E3B393900D1CB28 /* SVGConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E1461E3B393900D1CB28 /* SVGConstants.swift */; }; 57E5E1AB1E3B393900D1CB28 /* SVGParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E1471E3B393900D1CB28 /* SVGParser.swift */; }; 57E5E1AC1E3B393900D1CB28 /* SVGParserError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E1481E3B393900D1CB28 /* SVGParserError.swift */; }; - 57E5E1AD1E3B393900D1CB28 /* SVGParserRegexHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E1491E3B393900D1CB28 /* SVGParserRegexHelper.swift */; }; 57E5E1AE1E3B393900D1CB28 /* SVGView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E14A1E3B393900D1CB28 /* SVGView.swift */; }; 57E5E1AF1E3B393900D1CB28 /* CAAnimationClosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E14C1E3B393900D1CB28 /* CAAnimationClosure.swift */; }; 57E5E1B01E3B393900D1CB28 /* CGFloat+Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E5E14D1E3B393900D1CB28 /* CGFloat+Double.swift */; }; @@ -950,7 +948,6 @@ 57E5E1461E3B393900D1CB28 /* SVGConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGConstants.swift; sourceTree = ""; }; 57E5E1471E3B393900D1CB28 /* SVGParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGParser.swift; sourceTree = ""; }; 57E5E1481E3B393900D1CB28 /* SVGParserError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGParserError.swift; sourceTree = ""; }; - 57E5E1491E3B393900D1CB28 /* SVGParserRegexHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGParserRegexHelper.swift; sourceTree = ""; }; 57E5E14A1E3B393900D1CB28 /* SVGView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGView.swift; sourceTree = ""; }; 57E5E14C1E3B393900D1CB28 /* CAAnimationClosure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAAnimationClosure.swift; sourceTree = ""; }; 57E5E14D1E3B393900D1CB28 /* CGFloat+Double.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGFloat+Double.swift"; sourceTree = ""; }; @@ -1731,7 +1728,6 @@ 5B1A8C7520A15F7300E5FFAE /* SVGNodeLayout.swift */, 57E5E1471E3B393900D1CB28 /* SVGParser.swift */, 57E5E1481E3B393900D1CB28 /* SVGParserError.swift */, - 57E5E1491E3B393900D1CB28 /* SVGParserRegexHelper.swift */, C4820B171F458D0E008CE0FF /* SVGSerializer.swift */, 57E5E14A1E3B393900D1CB28 /* SVGView.swift */, ); @@ -2811,7 +2807,6 @@ 57614B671F83D15600875933 /* ContentsInterpolation.swift in Sources */, 57614B681F83D15600875933 /* GroupRenderer.swift in Sources */, 5B6E192820AC58F900454E7E /* RadialGradient.swift in Sources */, - 57614B691F83D15600875933 /* SVGParserRegexHelper.swift in Sources */, 5B6E192A20AC58F900454E7E /* Align.swift in Sources */, 57614B6B1F83D15600875933 /* NSTimer+Closure.swift in Sources */, 5B6E193620AC58F900454E7E /* Stop.swift in Sources */, @@ -2954,7 +2949,6 @@ 57A27BCF1E44C4EC0057BD3A /* ContentsInterpolation.swift in Sources */, 57E5E1A31E3B393900D1CB28 /* GroupRenderer.swift in Sources */, 5B6E192720AC58F900454E7E /* RadialGradient.swift in Sources */, - 57E5E1AD1E3B393900D1CB28 /* SVGParserRegexHelper.swift in Sources */, 5B6E192920AC58F900454E7E /* Align.swift in Sources */, 57E5E1B11E3B393900D1CB28 /* NSTimer+Closure.swift in Sources */, 30FF4962215CE97300FF653C /* MCAMediaTimingFillMode_iOS.swift in Sources */, diff --git a/Source/svg/SVGParser.swift b/Source/svg/SVGParser.swift index 5bb08a77..6c79ea89 100644 --- a/Source/svg/SVGParser.swift +++ b/Source/svg/SVGParser.swift @@ -534,79 +534,83 @@ open class SVGParser { fileprivate func parseTransformationAttribute(_ attributes: String, transform: Transform = Transform()) -> Transform { - guard let matcher = SVGParserRegexHelper.getTransformAttributeMatcher() else { - return transform - } + // Transform attribute regular grammar (whitespace characters are ignored): + // ([a-zA-Z]+)\(((-?\d+\.?\d*e?-?\d*,?)+)\) + // Group (1) is an attribute name. + // Group (2) is comma-separated numbers. + + var transform = transform + let scanner = Scanner(string: attributes) - let attributes = attributes.replacingOccurrences(of: "\n", with: "") - var finalTransform = transform - let fullRange = NSRange(location: 0, length: attributes.count) + stopParse: while !scanner.isAtEnd { + guard let attributeName = scanner.scannedCharacters(from: .transformationAttributeCharacters), + scanner.scanString("(", into: nil), + let valuesString = scanner.scannedUpToString(")"), + scanner.scanString(")", into: nil) else { + break stopParse + } - if let matchedAttribute = matcher.firstMatch(in: attributes, options: .reportCompletion, range: fullRange) { + // Skip an optional comma after ")". + _ = scanner.scanString(",", into: nil) - let attributeName = (attributes as NSString).substring(with: matchedAttribute.range(at: 1)) - let values = parseTransformValues((attributes as NSString).substring(with: matchedAttribute.range(at: 2))) + let values = parseTransformValues(valuesString) if values.isEmpty { return transform } + switch attributeName { case "translate": - if let x = Double(values[0]) { - var y: Double = 0 - if values.indices.contains(1) { - y = Double(values[1]) ?? 0 - } - finalTransform = transform.move(dx: x, dy: y) + let x = values[0] + var y: Double = 0 + if values.indices ~= 1 { + y = values[1] } + transform = transform.move(dx: x, dy: y) case "scale": - if let x = Double(values[0]) { - var y: Double = x - if values.indices.contains(1) { - y = Double(values[1]) ?? x - } - finalTransform = transform.scale(sx: x, sy: y) + let x = values[0] + var y: Double = x + if values.indices ~= 1 { + y = values[1] } + transform = transform.scale(sx: x, sy: y) case "rotate": - if let angle = Double(values[0]) { - if values.count == 1 { - finalTransform = transform.rotate(angle: degreesToRadians(angle)) - } else if values.count == 3 { - if let x = Double(values[1]), let y = Double(values[2]) { - finalTransform = transform.move(dx: x, dy: y).rotate(angle: degreesToRadians(angle)).move(dx: -x, dy: -y) - } - } + let angle = values[0] + if values.count == 1 { + transform = transform.rotate(angle: degreesToRadians(angle)) + } else if values.count == 3 { + let x = values[1] + let y = values[2] + transform = transform + .move(dx: x, dy: y) + .rotate(angle: degreesToRadians(angle)) + .move(dx: -x, dy: -y) } case "skewX": - if let x = Double(values[0]) { - let v = tan((x * Double.pi) / 180.0) - finalTransform = transform.shear(shx: v, shy: 0) - } + let x = values[0] + let v = tan((x * Double.pi) / 180.0) + transform = transform.shear(shx: v, shy: 0) case "skewY": - if let y = Double(values[0]) { - let y = tan((y * Double.pi) / 180.0) - finalTransform = transform.shear(shx: 0, shy: y) - } + let y = values[0] + let v = tan((y * Double.pi) / 180.0) + transform = transform.shear(shx: 0, shy: v) case "matrix": if values.count != 6 { return transform } - if let m11 = Double(values[0]), let m12 = Double(values[1]), - let m21 = Double(values[2]), let m22 = Double(values[3]), - let dx = Double(values[4]), let dy = Double(values[5]) { - - let transformMatrix = Transform(m11: m11, m12: m12, m21: m21, m22: m22, dx: dx, dy: dy) - finalTransform = transform.concat(with: transformMatrix) - } + let m11 = values[0] + let m12 = values[1] + let m21 = values[2] + let m22 = values[3] + let dx = values[4] + let dy = values[5] + let transformMatrix = Transform(m11: m11, m12: m12, m21: m21, m22: m22, dx: dx, dy: dy) + transform = transform.concat(with: transformMatrix) default: - break + break stopParse } - let rangeToRemove = NSRange(location: 0, - length: matchedAttribute.range.location + matchedAttribute.range.length) - let newAttributeString = (attributes as NSString).replacingCharacters(in: rangeToRemove, with: "") - return parseTransformationAttribute(newAttributeString, transform: finalTransform) - } else { - return transform } + + return transform } /// Parse an RGB @@ -639,20 +643,21 @@ open class SVGParser { b: Int(blue.rounded(.up))) } - fileprivate func parseTransformValues(_ values: String, collectedValues: [String] = []) -> [String] { - guard let matcher = SVGParserRegexHelper.getTransformMatcher() else { - return collectedValues - } - var updatedValues: [String] = collectedValues - let fullRange = NSRange(location: 0, length: values.count) - if let matchedValue = matcher.firstMatch(in: values, options: .reportCompletion, range: fullRange) { - let value = (values as NSString).substring(with: matchedValue.range) - updatedValues.append(value) - let rangeToRemove = NSRange(location: 0, length: matchedValue.range.location + matchedValue.range.length) - let newValues = (values as NSString).replacingCharacters(in: rangeToRemove, with: "") - return parseTransformValues(newValues, collectedValues: updatedValues) + fileprivate func parseTransformValues(_ values: String) -> [Double] { + // Parse comma-separated list of numbers. + var collectedValues: [Double] = [] + let scanner = Scanner(string: values) + + while !scanner.isAtEnd { + if let value = scanner.scannedDouble() { + collectedValues.append(value) + } else { + break + } + _ = scanner.scanString(",", into: nil) } - return updatedValues + + return collectedValues } fileprivate func getStyleAttributes(_ groupAttributes: [String: String], @@ -1042,28 +1047,19 @@ open class SVGParser { fontWeight: fontWeight, pos: pos) } else { - guard let matcher = SVGParserRegexHelper.getTextElementMatcher() else { - return .none - } - let elementString = element.description - let fullRange = NSRange(location: 0, length: elementString.count) - if let match = matcher.firstMatch(in: elementString, options: .reportCompletion, range: fullRange) { - let tspans = (elementString as NSString).substring(with: match.range(at: 1)) - let rect = Rect(x: getDoubleValue(element, attribute: "x") ?? 0, - y: getDoubleValue(element, attribute: "y") ?? 0) - let collectedTspans = collectTspans(tspans, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: rect) - return Group(contents: collectedTspans, place: pos, tag: getTag(element)) - } + let rect = Rect(x: getDoubleValue(element, attribute: "x") ?? 0, + y: getDoubleValue(element, attribute: "y") ?? 0) + let collectedTspans = collectTspans(element.children, + textAnchor: textAnchor, + fill: fill, + stroke: stroke, + opacity: opacity, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + bounds: rect) + return Group(contents: collectedTspans, place: pos, tag: getTag(element)) } - return .none } fileprivate func anchorToAlign(_ textAnchor: String?) -> Align { @@ -1103,9 +1099,7 @@ open class SVGParser { // REFACTOR - fileprivate func collectTspans(_ tspan: String, - collectedTspans: [Node] = [], - withWhitespace: Bool = false, + fileprivate func collectTspans(_ contents: [XMLContent], textAnchor: String?, fill: Fill?, stroke: Stroke?, @@ -1114,99 +1108,78 @@ open class SVGParser { fontSize: Int?, fontWeight: String?, bounds: Rect) -> [Node] { - let fullString = tspan.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) as NSString - // exit recursion - if fullString.isEqual(to: "") { - return collectedTspans - } - var collection = collectedTspans - let tagRange = fullString.range(of: " element - let closingTagRange = fullString.range(of: "".lowercased()) - let tspanString = fullString.substring(to: closingTagRange.location + closingTagRange.length) - let tspanXml = SWXMLHash.parse(tspanString) - guard let indexer = tspanXml.children.first, - let text = parseTspan(indexer, - withWhitespace: withWhitespace, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: bounds, - previousCollectedTspan: collection.last) else { - - // skip this element if it can't be parsed - return collectTspans(fullString.substring(from: closingTagRange.location + closingTagRange.length), - collectedTspans: collectedTspans, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: bounds) - } - collection.append(text) - let nextString = fullString.substring(from: closingTagRange.location + closingTagRange.length) as NSString - var withWhitespace = false - if nextString.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines).location == 0 { - withWhitespace = true - } - return collectTspans(fullString.substring(from: closingTagRange.location + closingTagRange.length), - collectedTspans: collection, - withWhitespace: withWhitespace, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: Rect(x: bounds.x, y: bounds.y, w: bounds.w + text.bounds.w, h: bounds.h)) - } - // parse as regular text element - var textString: NSString - if tagRange.location >= fullString.length { - textString = fullString - } else { - textString = fullString.substring(to: tagRange.location) as NSString - } - var nextStringWhitespace = false - var trimmedString = textString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - if trimmedString.count != textString.length { - nextStringWhitespace = true - } - trimmedString = withWhitespace ? " \(trimmedString)" : trimmedString - let text = Text(text: trimmedString, - font: getFont(fontName: fontName, fontWeight: fontWeight, fontSize: fontSize), - fill: fill, - stroke: stroke, - align: anchorToAlign(textAnchor), - baseline: .alphabetic, - place: Transform().move(dx: bounds.x + bounds.w, dy: bounds.y), opacity: opacity) - collection.append(text) - if tagRange.location >= fullString.length { // leave recursion - return collection - } - return collectTspans(fullString.substring(from: tagRange.location), - collectedTspans: collection, - withWhitespace: nextStringWhitespace, - textAnchor: textAnchor, - fill: fill, - stroke: stroke, - opacity: opacity, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - bounds: Rect(x: bounds.x, y: bounds.y, w: bounds.w + text.bounds.w, h: bounds.h)) - } - - fileprivate func parseTspan(_ tspan: XMLIndexer, + var collection: [Node] = [] + var bounds = bounds + // Whether to add a space before the next non-whitespace-only text. + var addWhitespace = false + // Whether to preserve leading whitespaces before the next text + // by adding a single space prefix. + var preserveWhitespace = false + + for element in contents { + let text: Text? + if let textElement = element as? TextElement { + // parse as regular text element + let textString = textElement.text + let hasLeadingWhitespace = textString.first?.isWhitespace == true + let hasTrailingWhitespace = textString.last?.isWhitespace == true + + var trimmedString = textString.trimmingCharacters(in: .whitespacesAndNewlines) + let isWhitespaceOnly = trimmedString.isEmpty + + if hasLeadingWhitespace && preserveWhitespace && !isWhitespaceOnly { + trimmedString = " " + trimmedString + } + + addWhitespace = preserveWhitespace && hasTrailingWhitespace + preserveWhitespace = false + + if trimmedString.isEmpty { + continue + } + + let place = Transform().move(dx: bounds.x + bounds.w, dy: bounds.y) + + text = Text(text: trimmedString, + font: getFont(fontName: fontName, fontWeight: fontWeight, fontSize: fontSize), + fill: fill, + stroke: stroke, + align: anchorToAlign(textAnchor), + baseline: .alphabetic, + place: place, + opacity: opacity) + } else if let tspanElement = element as? XMLElement, + tspanElement.name == "tspan" { + // parse as element + // ultimately skip it if it cannot be parsed + text = parseTspan(tspanElement, + withWhitespace: addWhitespace, + textAnchor: textAnchor, + fill: fill, + stroke: stroke, + opacity: opacity, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + bounds: bounds, + previousCollectedTspan: collection.last) + preserveWhitespace = true + addWhitespace = false + } else { + print("Skipped an unexpected element type: \(type(of: element)).") + text = nil + } + + if let text = text { + collection.append(text) + bounds = Rect(x: bounds.x, y: bounds.y, w: bounds.w + text.bounds.w, h: bounds.h) + } + } + + return collection + } + + fileprivate func parseTspan(_ element: XMLElement, withWhitespace: Bool = false, textAnchor: String?, fill: Fill?, @@ -1218,10 +1191,6 @@ open class SVGParser { bounds: Rect, previousCollectedTspan: Node?) -> Text? { - guard let element = tspan.element else { - return .none - } - let string = element.text var shouldAddWhitespace = withWhitespace let pos = getTspanPosition(element, @@ -1649,40 +1618,37 @@ open class SVGParser { } fileprivate func dimensionFromString(_ string: String) -> SVGLength? { + if string.hasSuffix("%"), let value = Double(string.dropLast()) { + return SVGLength(percent: value) + } if let value = doubleFromString(string) { return SVGLength(pixels: value) } - if string.hasSuffix("%") { - return SVGLength(percent: Double(string.dropLast())!) - } return .none } fileprivate func doubleFromString(_ string: String) -> Double? { - if let doubleValue = Double(string) { - return doubleValue - } if string == "none" { return 0 } - guard let matcher = SVGParserRegexHelper.getUnitsIdenitifierMatcher() else { + + let scanner = Scanner(string: string) + let value = scanner.scannedDouble() + let unit = scanner.scannedCharacters(from: .unitCharacters) + + if !scanner.isAtEnd { + let junk = scanner.scannedUpToCharacters(from: []) ?? "" + print("Found trailing junk \"\(junk)\" in string \"\(string)\".") return .none } - let fullRange = NSRange(location: 0, length: string.count) - if let match = matcher.firstMatch(in: string, options: .reportCompletion, range: fullRange) { - let unitString = (string as NSString).substring(with: match.range(at: 1)) - let numberString = String(string.dropLast(unitString.count)) - let value = Double(numberString) ?? 0 - switch unitString { - case "px" : - return value - default: - print("SVG parsing error. Unit \(unitString) not supported") - return value - } + switch unit { + case nil, "px": + return value + default: + print("SVG parsing error. Unit \"\(unit ?? "")\" is not supported") + return value } - return .none } fileprivate func getDoubleValueFromPercentage(_ element: SWXMLHash.XMLElement, attribute: String) -> Double? { @@ -2189,3 +2155,55 @@ fileprivate enum SVGKeys { static let color = "color" static let currentColor = "currentColor" } + +fileprivate extension Scanner { + /// A version of `scanDouble()`, available for an earlier OS. + func scannedDouble() -> Double? { + if #available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + return scanDouble() + } else { + var double: Double = 0 + return scanDouble(&double) ? double : nil + } + } + + /// A version of `scanCharacters(from:)`, available for an earlier OS. + func scannedCharacters(from set: CharacterSet) -> String? { + if #available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + return scanCharacters(from: set) + } else { + var string: NSString? = nil + return scanCharacters(from: set, into: &string) ? string as String? : nil + } + } + + /// A version of `scanUpToCharacters(from:)`, available for an earlier OS. + func scannedUpToCharacters(from set: CharacterSet) -> String? { + if #available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + return scanUpToCharacters(from: set) + } else { + var string: NSString? = nil + return scanUpToCharacters(from: set, into: &string) ? string as String? : nil + } + } + + /// A version of `scanUpToString(_:)`, available for an earlier OS. + func scannedUpToString(_ substring: String) -> String? { + if #available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + return scanUpToString(substring) + } else { + var string: NSString? = nil + return scanUpTo(substring, into: &string) ? string as String? : nil + } + } +} + +fileprivate extension CharacterSet { + /// Latin alphabet characters. + static let latinAlphabet = CharacterSet(charactersIn: "a"..."z") + .union(CharacterSet(charactersIn: "A"..."Z")) + + static let unitCharacters = CharacterSet.latinAlphabet + + static let transformationAttributeCharacters = CharacterSet.latinAlphabet +} diff --git a/Source/svg/SVGParserRegexHelper.swift b/Source/svg/SVGParserRegexHelper.swift deleted file mode 100644 index d50e0cc8..00000000 --- a/Source/svg/SVGParserRegexHelper.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation - -class SVGParserRegexHelper { - - fileprivate static let transformAttributePattern = "([a-z]+)\\(((\\-?\\d+\\.?\\d*e?\\-?\\d*\\s*,?\\s*)+)\\)" - fileprivate static let transformPattern = "\\-?\\d+\\.?\\d*e?\\-?\\d*" - fileprivate static let textElementPattern = "((?s:.*))<\\/text>" - fileprivate static let maskIdenitifierPattern = "url\\(#((?s:.*))\\)" - fileprivate static let unitsIdenitifierPattern = "([a-zA-Z]+)$" - - fileprivate static var transformMatcher: NSRegularExpression? - fileprivate static var transformAttributeMatcher: NSRegularExpression? - fileprivate static var textElementMatcher: NSRegularExpression? - fileprivate static var maskIdenitifierMatcher: NSRegularExpression? - fileprivate static var unitsMatcher: NSRegularExpression? - - class func getTransformAttributeMatcher() -> NSRegularExpression? { - if self.transformAttributeMatcher == nil { - do { - self.transformAttributeMatcher = try NSRegularExpression(pattern: transformAttributePattern, options: .caseInsensitive) - } catch { - - } - } - return self.transformAttributeMatcher - } - - class func getTransformMatcher() -> NSRegularExpression? { - if self.transformMatcher == nil { - do { - self.transformMatcher = try NSRegularExpression(pattern: transformPattern, options: .caseInsensitive) - } catch { - - } - } - return self.transformMatcher - } - - class func getTextElementMatcher() -> NSRegularExpression? { - if self.textElementMatcher == nil { - do { - self.textElementMatcher = try NSRegularExpression(pattern: textElementPattern, options: .caseInsensitive) - } catch { - - } - } - return self.textElementMatcher - } - - class func getMaskIdenitifierMatcher() -> NSRegularExpression? { - if self.maskIdenitifierMatcher == nil { - do { - self.maskIdenitifierMatcher = try NSRegularExpression(pattern: maskIdenitifierPattern, options: .caseInsensitive) - } catch { - - } - } - return self.maskIdenitifierMatcher - } - - class func getUnitsIdenitifierMatcher() -> NSRegularExpression? { - if unitsMatcher == nil { - do { - unitsMatcher = try NSRegularExpression(pattern: unitsIdenitifierPattern, options: .caseInsensitive) - } catch { - - } - } - return unitsMatcher - } - -}