diff --git a/Cloudinary/Classes/Core/Features/Helpers/CLDExpression.swift b/Cloudinary/Classes/Core/Features/Helpers/CLDExpression.swift index aa3c8cf0..dd928c69 100644 --- a/Cloudinary/Classes/Core/Features/Helpers/CLDExpression.swift +++ b/Cloudinary/Classes/Core/Features/Helpers/CLDExpression.swift @@ -36,9 +36,9 @@ import Foundation case initial_height case initialHeight + case initial_aspect_ratio case aspect_ratio case aspectRatio - case initial_aspect_ratio case initialAspectRatio case page_count @@ -73,9 +73,9 @@ import Foundation case .initial_height: return "ih" case .initialHeight : return "ih" + case .initial_aspect_ratio: return "iar" case .aspect_ratio : return "ar" case .aspectRatio : return "ar" - case .initial_aspect_ratio: return "iar" case .initialAspectRatio : return "iar" case .page_count: return "pc" @@ -104,9 +104,11 @@ import Foundation internal var currentValue : String internal var currentKey : String - - private let consecutiveDashesRegex: String = "[ _]+" - + + private var allSpaceAndOrDash : Bool = false + private let consecutiveDashesRegex : String = "[ _]+" + private let userVariableRegex : String = "\\$_*[^_]+" + // MARK: - Init public override init() { self.currentKey = String() @@ -118,6 +120,9 @@ import Foundation var components = value.components(separatedBy: .whitespacesAndNewlines) self.currentKey = components.removeFirst() self.currentValue = components.joined(separator: CLDVariable.elementsSeparator) + let range = NSRange(location: 0, length: value.utf16.count) + let regex = try? NSRegularExpression(pattern: "^" + consecutiveDashesRegex) + self.allSpaceAndOrDash = !(regex?.firstMatch(in: value, options: [], range: range) == nil) super.init() } @@ -286,14 +291,19 @@ import Foundation // MARK: - provide content public func asString() -> String { - guard !currentKey.isEmpty && !currentValue.isEmpty else { - + guard !currentKey.isEmpty else { + if allSpaceAndOrDash { + return "_" + } return String() } - - let key = replaceAllExpressionKeys(in: currentKey) - let value = removeExtraDashes(from: replaceAllUnencodeChars(in: currentValue)) - + + let key = removeExtraDashes(from: replaceAllExpressionKeys(in: currentKey)) + + if currentValue.isEmpty { + return "\(key)" + } + let value = removeExtraDashes(from: replaceAllUnencodedChars(in: currentValue)) return "\(key)_\(value)" } @@ -305,7 +315,7 @@ import Foundation } let key = replaceAllExpressionKeys(in: currentKey) - let value = removeExtraDashes(from: replaceAllUnencodeChars(in: currentValue)) + let value = removeExtraDashes(from: replaceAllUnencodedChars(in: currentValue)) return [key:value] } @@ -320,7 +330,7 @@ import Foundation } // MARK: - Private methods - private func replaceAllUnencodeChars(in string: String) -> String { + private func replaceAllUnencodedChars(in string: String) -> String { var wipString = string wipString = replaceAllOperators(in: string) @@ -353,26 +363,34 @@ import Foundation return wipString } - + private func replace(expressionKey: ExpressionKeys, in string: String) -> String { - - var result : String - + + var result : String! + let string = removeExtraDashes(from: string) + if string.contains(CLDVariable.variableNamePrefix) { - - result = string.components(separatedBy: CLDVariable.elementsSeparator).map({ - - let temp : String - switch $0.hasPrefix(CLDVariable.variableNamePrefix) { - case true : temp = $0 - case false: temp = $0.replacingOccurrences(of: expressionKey.rawValue, with: expressionKey.asString) + let range = NSRange(location: 0, length: string.utf16.count) + let regex = try? NSRegularExpression(pattern: userVariableRegex) + let allRanges = regex?.matches(in: string, options: [], range: range).map({ $0.range }) ?? [] + + // Replace substring in between user variables. e.x $initial_aspect_ratio_$width, only '_aspect_ratio_' will be addressed. + for (index, range) in allRanges.enumerated() { + let location = range.length + range.location + var length = range.length + + if index + 1 == allRanges.count { + length = string.count + } else { + let nextRange = allRanges[index + 1] + length = nextRange.location } - return temp - - }).joined(separator: CLDVariable.elementsSeparator) - + + if let stringRange = Range(NSRange(location: location, length: length - location), in: string) { + result = string.replacingOccurrences(of: expressionKey.rawValue, with: expressionKey.asString, options: .regularExpression, range: stringRange) + } + } } else { - result = string.replacingOccurrences(of: expressionKey.rawValue, with: expressionKey.asString) } return result @@ -404,3 +422,4 @@ import Foundation return string.replacingOccurrences(of: consecutiveDashesRegex, with: CLDVariable.elementsSeparator, options: .regularExpression, range: nil) } } + diff --git a/Cloudinary/Classes/Core/Features/Helpers/Layers/CLDTextLayer.swift b/Cloudinary/Classes/Core/Features/Helpers/Layers/CLDTextLayer.swift index fe34b61c..0eddf86e 100644 --- a/Cloudinary/Classes/Core/Features/Helpers/Layers/CLDTextLayer.swift +++ b/Cloudinary/Classes/Core/Features/Helpers/Layers/CLDTextLayer.swift @@ -33,6 +33,7 @@ import Foundation internal var fontWeight: String? internal var textDecoration: String? internal var textAlign: String? + internal var textStyle: String? internal var stroke: String? internal var letterSpacing: String? internal var lineSpacing: String? @@ -66,6 +67,32 @@ import Foundation return self } + /** + Sets a text style identifier. + Note: If this is used, all other style attributes are ignored in favor of this identifier + + - parameter text: A variable string or an explicit style (e.g. "Arial_17_bold_antialias_best"). + + - returns: The same instance of CLDTextLayer. + */ + open func setTextStyle(textStyle: String) -> CLDTextLayer { + self.textStyle = textStyle + return self + } + + /** + Sets a text style identifier using an expression. + Note: If this is used, all other style attributes are ignored in favor of this identifier + + - parameter text: An expression instance referencing the style. + + - returns: The same instance of CLDTextLayer. + */ + open func setTextStyle(expression: CLDExpression) -> CLDTextLayer { + self.textStyle = expression.asString() + return self + } + /** Set the name of a font family. e.g. `arial`. @@ -333,7 +360,10 @@ import Foundation let optionalTextParams = getOptionalTextPropertiesArray() let mandatoryTextParams = getMandatoryTextPropertiesArray() - if optionalTextParams.isEmpty { + if let textStyle = textStyle { + components.append([textStyle].joined(separator: "_")) + } + else if optionalTextParams.isEmpty { if !mandatoryTextParams.isEmpty { components.append(mandatoryTextParams.joined(separator: "_")) } diff --git a/Example/Tests/GenerateUrlTests/UrlTests.swift b/Example/Tests/GenerateUrlTests/UrlTests.swift index 45ef0d34..173ccbd6 100644 --- a/Example/Tests/GenerateUrlTests/UrlTests.swift +++ b/Example/Tests/GenerateUrlTests/UrlTests.swift @@ -1052,6 +1052,29 @@ class UrlTests: BaseTestCase { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDFetchLayer(url: "https://res.cloudinary.com/demo/image/upload/sample"))).generate("test"), "\(prefix)/image/upload/l_fetch:aHR0cHM6Ly9yZXMuY2xvdWRpbmFyeS5jb20vZGVtby9pbWFnZS91cGxvYWQvc2FtcGxl/test") } + + func testTextStyle() { + XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVariable("$style", string: "!Arial_12!").chain().setOverlayWithLayer(CLDTextLayer().setText(text: "hello-world").setTextStyle(textStyle: "$style"))).generate("test"), "\(prefix)/image/upload/$style_!Arial_12!/l_text:$style:hello-world/test") + + XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVariable("$style", string: "!Arial_12!").chain().setOverlayWithLayer(CLDTextLayer().setText(text: "hello-world").setTextStyle(expression: CLDExpression(value: "$style")))).generate("test"), "\(prefix)/image/upload/$style_!Arial_12!/l_text:$style:hello-world/test") + } + + func testTextStyleOverpowerOtherTextAttributes() { + let layer = CLDTextLayer() + .setText(text: "hello_world") + .setFontFamily(fontFamily: "Arial") + .setFontSize(18) + .setFontStyle(.italic) + .setFontWeight(.bold) + .setLetterSpacing(4) + .setTextStyle(textStyle: "$style") + + let transformation = CLDTransformation().setVariable("$style", string: "!Arial_12!").chain().setOverlayWithLayer(layer) + let url = sut?.createUrl().setTransformation(transformation).generate("test") + + XCTAssertNotNil(url) + XCTAssertFalse(url!.contains("Arial_18_bold_italic_letter_spacing_4"), "$style should overpower other text attributes.") + } func testOverlayErrors() { XCTAssertNil(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDTextLayer().setText(text: "text").setFontStyle(.italic))).generate("test")) diff --git a/Example/Tests/TransformationTests/CLDExpressionTests/CLDExpressionTests.swift b/Example/Tests/TransformationTests/CLDExpressionTests/CLDExpressionTests.swift index f28494ac..e63ba84a 100644 --- a/Example/Tests/TransformationTests/CLDExpressionTests/CLDExpressionTests.swift +++ b/Example/Tests/TransformationTests/CLDExpressionTests/CLDExpressionTests.swift @@ -877,4 +877,339 @@ class CLDExpressionTests: BaseTestCase { // Then XCTAssertEqual(actualResult, expectedResult, "Calling asParams, should remove extra dashes/spaces") } + + func test_powerString_shouldAppendValidValuex() { + // Given + let name = "$________height" + let initialValue = "$_____width" + let value = "30.3" + + let expectedValueResult = "$_height_$_width_pow_30.3" + + // When + sut = CLDExpression(value: "\(name) \(initialValue)").power(by: value) + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionNumber() { + // Given + let name = 10 + let expectedValueResult = "10" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionSingleSpace_underscore() { + // Given + let name = " " + let expectedValueResult = "_" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionBlankString_underscore() { + // Given + let name = " " + let expectedValueResult = "_" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionUnderscore_underscore() { + // Given + let name = "_" + let expectedValueResult = "_" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionUnderscores_underscore() { + // Given + let name = "___" + let expectedValueResult = "_" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionUnderscoresAndSpaces_underscore() { + // Given + let name = " _ __ _" + let expectedValueResult = "_" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionArbitraryText_isNotAffected() { + // Given + let name = "foobar" + let expectedValueResult = "foobar" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoubleAmpersand_replacedWithAndOperator() { + // Given + let name = "foo && bar" + let expectedValueResult = "foo_and_bar" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoubleAmpersandWithNoSpaceAtEnd_isNotAffected() { + // Given + let name = "foo&&bar" + let expectedValueResult = "foo&&bar" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionWidth_recognizedAsVariableAndReplacedWithW() { + // Given + let name = "width" + let expectedValueResult = "w" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionInitialAspectRatio_recognizedAsVariableAndReplacedWithW() { + // Given + let name = "width" + let expectedValueResult = "w" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDollarWidth_recognizedAsUserVariableAndNotAffected() { + // Given + let name = "$width" + let expectedValueResult = "$width" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDollarInitialAspectRatio_recognizedAsUserVariableAndAsVariableReplacedWithAr() { + // Given + let name = "$initial_aspect_ratio" + let expectedValueResult = "$initial_ar" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDollarMyWidth_recognizedAsUserVariableAndNotAffected() { + // Given + let name = "$mywidth" + let expectedValueResult = "$mywidth" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDollarWidthWidth_recognizedAsUserVariableAndNotAffected() { + // Given + let name = "$widthwidth" + let expectedValueResult = "$widthwidth" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDollarUnderscoreWidth_recognizedAsUserVariableAndNotAffected() { + // Given + let name = "$_width" + let expectedValueResult = "$_width" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDollarUnderscoreX2Width_recognizedAsUserVariableAndNotAffected() { + // Given + let name = "$__width" + let expectedValueResult = "$_width" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDollarX2Width_recognizedAsUserVariableAndNotAffected() { + // Given + let name = "$\\$width" + let expectedValueResult = "$\\$width" + + // When + sut = CLDExpression(value: "\(name)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoesntReplaceVariable_1() { + // Given + let name = "$height" + let value = "100" + let expectedValueResult = "$height_100" + + // When + sut = CLDExpression(value: "\(name) \(value)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoesntReplaceVariable_2() { + // Given + let name = "$heightt" + let value = "100" + let expectedValueResult = "$heightt_100" + + // When + sut = CLDExpression(value: "\(name) \(value)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoesntReplaceVariable_3() { + // Given + let name = "$\\$height" + let value = "100" + let expectedValueResult = "$\\$height_100" + + // When + sut = CLDExpression(value: "\(name) \(value)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoesntReplaceVariable_4() { + // Given + let name = "$heightmy" + let value = "100" + let expectedValueResult = "$heightmy_100" + + // When + sut = CLDExpression(value: "\(name) \(value)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoesntReplaceVariable_5() { + // Given + let name = "$myheight" + let value = "100" + let expectedValueResult = "$myheight_100" + + // When + sut = CLDExpression(value: "\(name) \(value)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoesntReplaceVariable_6() { + // Given + let name = "$heightheight" + let value = "100" + let expectedValueResult = "$heightheight_100" + + // When + sut = CLDExpression(value: "\(name) \(value)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoesntReplaceVariable_7() { + // Given + let name = "$theheight" + let value = "100" + let expectedValueResult = "$theheight_100" + + // When + sut = CLDExpression(value: "\(name) \(value)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } + + func testExpressionDoesntReplaceVariable_8() { + // Given + let name = "$________height" + let value = "100" + let expectedValueResult = "$_height_100" + + // When + sut = CLDExpression(value: "\(name) \(value)") + + // Then + XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") + } }