From 2469ea8a3d19d651ee6c772d412a7d1d406981fb Mon Sep 17 00:00:00 2001 From: Raya Straus Date: Sun, 15 Aug 2021 10:33:43 +0300 Subject: [PATCH 1/3] added backwards compatibility object processing --- __TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts | 4 +- .../createLegacyURL.test.ts | 69 ++ .../legacyLayerURL.test.ts | 72 ++ jest.config.json | 3 +- src/backwards/condition.ts | 102 ++ src/backwards/configuration.ts | 234 ++++ src/backwards/expression.ts | 342 ++++++ src/backwards/generateTransformationString.ts | 9 +- src/backwards/legacyLayer/fetchlayer.ts | 38 + src/backwards/legacyLayer/layer.ts | 108 ++ src/backwards/legacyLayer/subtitleslayer.ts | 15 + src/backwards/legacyLayer/textlayer.ts | 176 +++ src/backwards/parameters.ts | 356 ++++++ src/backwards/transformation.ts | 1060 +++++++++++++++++ .../transformationProcessing/processDpr.ts | 14 + .../transformationProcessing/processLayer.ts | 5 + src/backwards/utils/isEmpty.ts | 6 + src/backwards/utils/isFunction.ts | 9 + src/backwards/utils/isNumberLike.ts | 14 + src/backwards/utils/legacyBaseUtil.ts | 74 ++ src/backwards/utils/snakeCase.ts | 5 + src/types/types.ts | 4 +- 22 files changed, 2713 insertions(+), 6 deletions(-) create mode 100644 __TESTS__/backwardsComaptibility/legacyLayerURL.test.ts create mode 100644 src/backwards/condition.ts create mode 100644 src/backwards/configuration.ts create mode 100644 src/backwards/expression.ts create mode 100644 src/backwards/legacyLayer/fetchlayer.ts create mode 100644 src/backwards/legacyLayer/layer.ts create mode 100644 src/backwards/legacyLayer/subtitleslayer.ts create mode 100644 src/backwards/legacyLayer/textlayer.ts create mode 100644 src/backwards/parameters.ts create mode 100644 src/backwards/transformation.ts create mode 100644 src/backwards/transformationProcessing/processDpr.ts create mode 100644 src/backwards/utils/isEmpty.ts create mode 100644 src/backwards/utils/isFunction.ts create mode 100644 src/backwards/utils/isNumberLike.ts create mode 100644 src/backwards/utils/legacyBaseUtil.ts create mode 100644 src/backwards/utils/snakeCase.ts diff --git a/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts b/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts index 348248e7..ab0e2b7f 100644 --- a/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts +++ b/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts @@ -67,14 +67,14 @@ const bundleSizeTestCases:ITestCase[] = [ }, { name: 'Import backwards comaptiblity function', - sizeLimitInKB: 26, + sizeLimitInKB: 56, importsArray: [ importFromBase('createCloudinaryLegacyURL') ] }, { name: 'Import all of the SDK', - sizeLimitInKB: 110, + sizeLimitInKB: 117, importsArray: [ importFromBase('CloudinaryBaseSDK') ] diff --git a/__TESTS__/backwardsComaptibility/createLegacyURL.test.ts b/__TESTS__/backwardsComaptibility/createLegacyURL.test.ts index 416665be..9cf2cc78 100644 --- a/__TESTS__/backwardsComaptibility/createLegacyURL.test.ts +++ b/__TESTS__/backwardsComaptibility/createLegacyURL.test.ts @@ -1,5 +1,6 @@ import {createTestURL} from "./transformationLegacyTests/utils/createTestURL"; import {createCloudinaryLegacyURL} from "../../src"; +import Transformation from "../../src/backwards/transformation"; describe('Create legacy urls', () => { it('Should throw without cloudName', () => { @@ -987,4 +988,72 @@ describe('Create legacy urls', () => { expect(urlFirstRun).toEqual("http://res.cloudinary.com/test123/image/upload/c_fill,h_120,l_somepid,w_80/sample"); expect(urlFirstRun).toEqual(urlSecondRun); }); + it("Transformation object: User Define Variables", function () { + const options = { + if: "face_count > 2", + variables: [["$z", 5], ["$foo", "$z * 2"]], + crop: "scale", + width: "$foo * 200" + }; + const result = createTestURL("sample", + { + transformation: new Transformation(options) + } + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/if_fc_gt_2,$z_5,$foo_$z_mul_2,c_scale,w_$foo_mul_200/sample'); + }); + + it("Transformation object: should sort variables", function () { + const result = createTestURL("sample", + { + transformation: new Transformation({ + $second: 1, + $first: 2 + }) + } + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/$first_2,$second_1/sample'); + }); + + it("Transformation object: string overlay", function () { + const result = createTestURL("sample", + { + transformation: new Transformation().overlay("text:hello").width(100).height(100) + } + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/h_100,l_text:hello,w_100/sample'); + }); + + it("Transformation object: object overlay", function () { + const options = { + text: "Cloudinary for the win!", + fontFamily: "Arial", + fontSize: 18, + fontAntialiasing: "fast" + }; + const result = createTestURL("sample", + { + transformation: new Transformation().overlay(options).width(100).height(100) + } + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/h_100,l_text:Arial_18_antialias_fast:Cloudinary%20for%20the%20win%21,w_100/sample'); + }); + + it("Transformation object: should support fetch:URL literal", function () { + const result = createTestURL("sample", + { + transformation: new Transformation().overlay("fetch:http://cloudinary.com/images/old_logo.png") + } + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/l_fetch:aHR0cDovL2Nsb3VkaW5hcnkuY29tL2ltYWdlcy9vbGRfbG9nby5wbmc=/sample'); + }); + + it("Transformation object: should support chained transformation", function () { + const result = createTestURL("sample", + { + transformation: new Transformation().width(100).crop("scale").chain().crop("crop").width(200) + } + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/c_scale,w_100/c_crop,w_200/sample'); + }); }); diff --git a/__TESTS__/backwardsComaptibility/legacyLayerURL.test.ts b/__TESTS__/backwardsComaptibility/legacyLayerURL.test.ts new file mode 100644 index 00000000..edf89772 --- /dev/null +++ b/__TESTS__/backwardsComaptibility/legacyLayerURL.test.ts @@ -0,0 +1,72 @@ +import {createTestURL} from "./transformationLegacyTests/utils/createTestURL"; +import Transformation from "../../src/backwards/transformation"; +import FetchLayer from "../../src/backwards/legacyLayer/fetchlayer"; +import TextLayer from "../../src/backwards/legacyLayer/textlayer"; +import Layer from "../../src/backwards/legacyLayer/layer"; + +describe('Create legacy layer urls', () => { + it("Should support Layer as overlay input", function () { + const result = createTestURL("sample", + {overlay: new Layer().resourceType("video").publicId("cat")} + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/l_video:cat/sample'); + }); + + it("Should support TextLayer as overlay input", function () { + const result = createTestURL("sample", + {overlay: new TextLayer().fontFamily("Arial").fontSize(80).text("Flowers")} + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/l_text:Arial_80:Flowers/sample'); + }); + + it("Should support TextLayer object", function () { + const options = { + text: "Cloudinary for the win!", + fontFamily: "Arial", + fontSize: 18, + fontHinting: "full" + }; + const result = createTestURL("sample", + {overlay: new TextLayer(options)} + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/l_text:Arial_18_hinting_full:Cloudinary%20for%20the%20win%21/sample'); + }); + + it("Should support FetchLayer: string", function () { + const result = createTestURL("sample", + { + transformation: new Transformation({ + overlay: new FetchLayer("http://cloudinary.com/images/logo.png") + }) + } + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/l_fetch:aHR0cDovL2Nsb3VkaW5hcnkuY29tL2ltYWdlcy9sb2dvLnBuZw==/sample'); + }); + + it("Should support FetchLayer: url object", function () { + const result = createTestURL("sample", + { + transformation: new Transformation({ + overlay: new FetchLayer({ + url: 'http://res.cloudinary.com/demo/sample.jpg' + }) + }) + } + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/l_fetch:aHR0cDovL3Jlcy5jbG91ZGluYXJ5LmNvbS9kZW1vL3NhbXBsZS5qcGc=/sample'); + }); + + it("Should change dpr to float", function () { + const result = createTestURL("sample", + {dpr: 1} + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/dpr_1.0/sample'); + }); + + it("Should change dpr to float on transformation input", function () { + const result = createTestURL("sample", + {transformation: new Transformation().dpr(1)} + ); + expect(result).toBe('http://res.cloudinary.com/demo/image/upload/dpr_1.0/sample'); + }); +}); diff --git a/jest.config.json b/jest.config.json index ed9dab70..3c70b8fc 100644 --- a/jest.config.json +++ b/jest.config.json @@ -6,7 +6,8 @@ "/src/**/*.ts", "/scripts/**/*.ts", "!/scripts/createEntrypoints.ts", - "!/scripts/copyPackageJsonToSrc.ts.ts" + "!/scripts/copyPackageJsonToSrc.ts.ts", + "!/src/backwards/**/*.ts" ], "modulePaths": [ "/src" diff --git a/src/backwards/condition.ts b/src/backwards/condition.ts new file mode 100644 index 00000000..f6a7cb1b --- /dev/null +++ b/src/backwards/condition.ts @@ -0,0 +1,102 @@ +import Expression from './expression'; + +/** + * Represents a transformation condition. + * @param {string} conditionStr - a condition in string format + * @class Condition + * @example + * // normally this class is not instantiated directly + * var tr = cloudinary.Transformation.new() + * .if().width( ">", 1000).and().aspectRatio("<", "3:4").then() + * .width(1000) + * .crop("scale") + * .else() + * .width(500) + * .crop("scale") + * + * var tr = cloudinary.Transformation.new() + * .if("w > 1000 and aspectRatio < 3:4") + * .width(1000) + * .crop("scale") + * .else() + * .width(500) + * .crop("scale") + * + */ +class Condition extends Expression { + constructor(conditionStr:string) { + super(conditionStr); + } + + /** + * @function Condition#height + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Condition} this condition + */ + height(operator: string, value: string|number) { + return this.predicate("h", operator, value); + } + + /** + * @function Condition#width + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Condition} this condition + */ + width(operator: string, value: string|number) { + return this.predicate("w", operator, value); + } + + /** + * @function Condition#aspectRatio + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Condition} this condition + */ + aspectRatio(operator: string, value: string|number) { + return this.predicate("ar", operator, value); + } + + /** + * @function Condition#pages + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Condition} this condition + */ + pageCount(operator: string, value: string|number) { + return this.predicate("pc", operator, value); + } + + /** + * @function Condition#faces + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Condition} this condition + */ + faceCount(operator: string, value: string|number) { + return this.predicate("fc", operator, value); + } + + /** + * @function Condition#duration + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Condition} this condition + */ + duration(operator: string, value: string|number) { + return this.predicate("du", operator, value); + } + + /** + * @function Condition#initialDuration + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Condition} this condition + */ + initialDuration(operator: string, value: string|number) { + return this.predicate("idu", operator, value); + } +} + +export default Condition; diff --git a/src/backwards/configuration.ts b/src/backwards/configuration.ts new file mode 100644 index 00000000..a1b43905 --- /dev/null +++ b/src/backwards/configuration.ts @@ -0,0 +1,234 @@ +import cloneDeep from 'lodash.clonedeep'; +import {isObject} from "./utils/isObject"; +/** + * Class for defining account configuration options. + * Depends on 'utils' + */ + + +/** + * Assign values from sources if they are not defined in the destination. + * Once a value is set it does not change + * @function Util.defaults + * @param {Object} destination - the object to assign defaults to + * @param sources + * @param {...Object} source - the source object(s) to assign defaults from + * @return {Object} destination after it was modified + */ +const defaults = (destination:{}, ...sources: object[])=>{ + return sources.reduce(function(dest, source) { + let key, value; + for (key in source) { + // @ts-ignore + value = source[key]; + // @ts-ignore + if (dest[key] === void 0) { + // @ts-ignore + dest[key] = value; + } + } + return dest; + }, destination); +}; + +/** + * Class for defining account configuration options. + * @constructor Configuration + * @param {Object} options - The account configuration parameters to set. + * @see Available configuration options + */ +class Configuration { + private configuration: any; + constructor(options: {}) { + this.configuration = options == null ? {} : cloneDeep(options); + defaults(this.configuration, DEFAULT_CONFIGURATION_PARAMS); + } + + /** + * Initializes the configuration. This method is a convenience method that invokes both + * {@link Configuration#fromEnvironment|fromEnvironment()} (Node.js environment only) + * and {@link Configuration#fromDocument|fromDocument()}. + * It first tries to retrieve the configuration from the environment variable. + * If not available, it tries from the document meta tags. + * @function Configuration#init + * @return {Configuration} returns `this` for chaining + * @see fromDocument + * @see fromEnvironment + */ + init() { + this.fromEnvironment(); + this.fromDocument(); + return this; + } + + /** + * Set a new configuration item + * @function Configuration#set + * @param {string} name - the name of the item to set + * @param {*} value - the value to be set + * @return {Configuration} + * + */ + set(name:string|boolean, value:any) { + // @ts-ignore + this.configuration[name] = value; + return this; + } + + /** + * Get the value of a configuration item + * @function Configuration#get + * @param {string} name - the name of the item to set + * @return {*} the configuration item + */ + get(name:string) { + return this.configuration[name]; + } + + merge(config:any) { + Object.assign(this.configuration, cloneDeep(config)); + return this; + } + + /** + * Initialize Cloudinary from HTML meta tags. + * @function Configuration#fromDocument + * @return {Configuration} + * @example + * + */ + fromDocument() { + var el, i, len, meta_elements; + meta_elements = typeof document !== "undefined" && document !== null ? document.querySelectorAll('meta[name^="cloudinary_"]') : void 0; + if (meta_elements) { + for (i = 0, len = meta_elements.length; i < len; i++) { + el = meta_elements[i]; + this.configuration[el.getAttribute('name').replace('cloudinary_', '')] = el.getAttribute('content'); + } + } + return this; + } + + /** + * Initialize Cloudinary from the `CLOUDINARY_URL` environment variable. + * + * This function will only run under Node.js environment. + * @function Configuration#fromEnvironment + * @requires Node.js + */ + fromEnvironment() { + var cloudinary_url, query, uri, uriRegex; + if(typeof process !== "undefined" && process !== null && process.env && process.env.CLOUDINARY_URL ){ + cloudinary_url = process.env.CLOUDINARY_URL; + uriRegex = /cloudinary:\/\/(?:(\w+)(?:\:([\w-]+))?@)?([\w\.-]+)(?:\/([^?]*))?(?:\?(.+))?/; + uri = uriRegex.exec(cloudinary_url); + if (uri) { + if (uri[3] != null) { + this.configuration['cloud_name'] = uri[3]; + } + if (uri[1] != null) { + this.configuration['api_key'] = uri[1]; + } + if (uri[2] != null) { + this.configuration['api_secret'] = uri[2]; + } + if (uri[4] != null) { + this.configuration['private_cdn'] = uri[4] != null; + } + if (uri[4] != null) { + this.configuration['secure_distribution'] = uri[4]; + } + query = uri[5]; + if (query != null) { + query.split('&').forEach(value=>{ + let [k, v] = value.split('='); + if (v == null) { + // @ts-ignore + v = true; + } + this.configuration[k] = v; + }); + } + } + } + return this; + } + + /** + * Create or modify the Cloudinary client configuration + * + * Warning: `config()` returns the actual internal configuration object. modifying it will change the configuration. + * + * This is a backward compatibility method. For new code, use get(), merge() etc. + * @function Configuration#config + * @param {hash|string|boolean} new_config + * @param {string} new_value + * @returns {*} configuration, or value + * + * @see {@link fromEnvironment} for initialization using environment variables + * @see {@link fromDocument} for initialization using HTML meta tags + */ + config(new_config:string, new_value:string) { + switch (false) { + case new_value === void 0: + this.set(new_config, new_value); + return this.configuration; + case typeof new_config != 'string': + return this.get(new_config); + case !isObject(new_config): + this.merge(new_config); + return this.configuration; + default: + // Backward compatibility - return the internal object + return this.configuration; + } + } + + /** + * Returns a copy of the configuration parameters + * @function Configuration#toOptions + * @returns {Object} a key:value collection of the configuration parameters + */ + toOptions() { + return cloneDeep(this.configuration); + } + +} + +const DEFAULT_CONFIGURATION_PARAMS = { + responsive_class: 'cld-responsive', + responsive_use_breakpoints: true, + round_dpr: true, + secure: (typeof window !== "undefined" && window !== null ? window.location ? window.location.protocol : void 0 : void 0) === 'https:' +}; + +export const CONFIG_PARAMS = [ + "api_key", + "api_secret", + "callback", + "cdn_subdomain", + "cloud_name", + "cname", + "private_cdn", + "protocol", + "resource_type", + "responsive", + "responsive_class", + "responsive_use_breakpoints", + "responsive_width", + "round_dpr", + "secure", + "secure_cdn_subdomain", + "secure_distribution", + "shorten", + "type", + "upload_preset", + "url_suffix", + "use_root_path", + "version", + "externalLibraries", + "max_timeout_ms" +]; + +export default Configuration; diff --git a/src/backwards/expression.ts b/src/backwards/expression.ts new file mode 100644 index 00000000..737d4f79 --- /dev/null +++ b/src/backwards/expression.ts @@ -0,0 +1,342 @@ +/** + * Represents a transformation expression. + * @param {string} expressionStr - An expression in string format. + * @class Expression + * + */ +class Expression { + private expressions: any[]; + private parent: any; + constructor(expressionStr: string) { + /** + * @protected + * @inner Expression-expressions + */ + this.expressions = []; + if (expressionStr != null) { + this.expressions.push(Expression.normalize(expressionStr)); + } + } + + /** + * Convenience constructor method + * @function Expression.new + */ + static new(expressionStr?:string) { + return new this(expressionStr); + } + + /** + * Normalize a string expression + * @function Cloudinary#normalize + * @param {string} expression a expression, e.g. "w gt 100", "width_gt_100", "width > 100" + * @return {string} the normalized form of the value expression, e.g. "w_gt_100" + */ + static normalize(expression:string | number) { + var operators, operatorsPattern, operatorsReplaceRE, predefinedVarsPattern, predefinedVarsReplaceRE; + if (expression == null) { + return expression; + } + expression = String(expression); + operators = "\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*|\\^"; + + // operators + operatorsPattern = "((" + operators + ")(?=[ _]))"; + operatorsReplaceRE = new RegExp(operatorsPattern, "g"); + // @ts-ignore + expression = expression.replace(operatorsReplaceRE, match => OPERATORS[match]); + + // predefined variables + predefinedVarsPattern = "(" + Object.keys(PREDEFINED_VARS).join("|") + ")"; + predefinedVarsReplaceRE = new RegExp(predefinedVarsPattern, "g"); + // @ts-ignore + expression = expression.replace(predefinedVarsReplaceRE, (match, p1, offset) => (expression[offset - 1] === '$' ? match : PREDEFINED_VARS[match])); + + return expression.replace(/[ _]+/g, '_'); + } + + /** + * Serialize the expression + * @return {string} the expression as a string + */ + serialize() { + return Expression.normalize(this.expressions.join("_")); + } + + toString() { + return this.serialize(); + } + + /** + * Get the parent transformation of this expression + * @return Transformation + */ + getParent() { + return this.parent; + } + + /** + * Set the parent transformation of this expression + * @param {Transformation} the parent transformation + * @return {Expression} this expression + */ + setParent(parent:any) { + this.parent = parent; + return this; + } + + /** + * Add a expression + * @function Expression#predicate + * @internal + */ + predicate(name: string, operator: string | number, value: any) { + // @ts-ignore + if (OPERATORS[operator] != null) { + // @ts-ignore + operator = OPERATORS[operator]; + } + this.expressions.push(`${name}_${operator}_${value}`); + return this; + } + + /** + * @function Expression#and + */ + and() { + this.expressions.push("and"); + return this; + } + + /** + * @function Expression#or + */ + or() { + this.expressions.push("or"); + return this; + } + + /** + * Conclude expression + * @function Expression#then + * @return {Transformation} the transformation this expression is defined for + */ + then() { + return this.getParent().if(this.toString()); + } + + /** + * @function Expression#height + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Expression} this expression + */ + height(operator: string, value: string|number) { + return this.predicate("h", operator, value); + } + + /** + * @function Expression#width + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Expression} this expression + */ + width(operator: string, value: string|number) { + return this.predicate("w", operator, value); + } + + /** + * @function Expression#aspectRatio + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Expression} this expression + */ + aspectRatio(operator: string, value: string|number) { + return this.predicate("ar", operator, value); + } + + /** + * @function Expression#pages + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Expression} this expression + */ + pageCount(operator: string, value: string|number) { + return this.predicate("pc", operator, value); + } + + /** + * @function Expression#faces + * @param {string} operator the comparison operator (e.g. "<", "lt") + * @param {string|number} value the right hand side value + * @return {Expression} this expression + */ + faceCount(operator: string, value: string|number) { + return this.predicate("fc", operator, value); + } + + value(value:string|number) { + this.expressions.push(value); + return this; + } + + /** + */ + static variable(name:string, value:string|number) { + return new this(name).value(value); + } + + /** + * @returns Expression a new expression with the predefined variable "width" + * @function Expression.width + */ + static width() { + return new this("width"); + } + + /** + * @returns Expression a new expression with the predefined variable "height" + * @function Expression.height + */ + static height() { + return new this("height"); + } + + /** + * @returns Expression a new expression with the predefined variable "initialWidth" + * @function Expression.initialWidth + */ + static initialWidth() { + return new this("initialWidth"); + } + + /** + * @returns Expression a new expression with the predefined variable "initialHeight" + * @function Expression.initialHeight + */ + static initialHeight() { + return new this("initialHeight"); + } + + /** + * @returns Expression a new expression with the predefined variable "aspectRatio" + * @function Expression.aspectRatio + */ + static aspectRatio() { + return new this("aspectRatio"); + } + + /** + * @returns Expression a new expression with the predefined variable "initialAspectRatio" + * @function Expression.initialAspectRatio + */ + static initialAspectRatio() { + return new this("initialAspectRatio"); + } + + /** + * @returns Expression a new expression with the predefined variable "pageCount" + * @function Expression.pageCount + */ + static pageCount() { + return new this("pageCount"); + } + + /** + * @returns Expression new expression with the predefined variable "faceCount" + * @function Expression.faceCount + */ + static faceCount() { + return new this("faceCount"); + } + + /** + * @returns Expression a new expression with the predefined variable "currentPage" + * @function Expression.currentPage + */ + static currentPage() { + return new this("currentPage"); + } + + /** + * @returns Expression a new expression with the predefined variable "tags" + * @function Expression.tags + */ + static tags() { + return new this("tags"); + } + + /** + * @returns Expression a new expression with the predefined variable "pageX" + * @function Expression.pageX + */ + static pageX() { + return new this("pageX"); + } + + /** + * @returns Expression a new expression with the predefined variable "pageY" + * @function Expression.pageY + */ + static pageY() { + return new this("pageY"); + } + +} + +/** + * @internal + */ +const OPERATORS = { + "=": 'eq', + "!=": 'ne', + "<": 'lt', + ">": 'gt', + "<=": 'lte', + ">=": 'gte', + "&&": 'and', + "||": 'or', + "*": "mul", + "/": "div", + "+": "add", + "-": "sub", + "^": "pow", +}; + +/** + * @internal + */ +const PREDEFINED_VARS = { + "aspect_ratio": "ar", + "aspectRatio": "ar", + "current_page": "cp", + "currentPage": "cp", + "preview:duration": "preview:duration", + "duration": "du", + "face_count": "fc", + "faceCount": "fc", + "height": "h", + "initial_aspect_ratio": "iar", + "initial_duration": "idu", + "initial_height": "ih", + "initial_width": "iw", + "initialAspectRatio": "iar", + "initialDuration": "idu", + "initialHeight": "ih", + "initialWidth": "iw", + "page_count": "pc", + "page_x": "px", + "page_y": "py", + "pageCount": "pc", + "pageX": "px", + "pageY": "py", + "tags": "tags", + "width": "w" +}; + +/** + * @internal + */ +const BOUNDRY = "[ _]+"; + +export default Expression; diff --git a/src/backwards/generateTransformationString.ts b/src/backwards/generateTransformationString.ts index 357f88a6..4c1943fc 100644 --- a/src/backwards/generateTransformationString.ts +++ b/src/backwards/generateTransformationString.ts @@ -11,6 +11,8 @@ import {splitRange} from "./utils/splitRange"; import {legacyNormalizeExpression} from "./utils/legacyNormalizeExpression"; import {normRangeValues} from "./utils/norm_range_values"; import {processVideoParams} from "./transformationProcessing/processVideoParams"; +import Transformation from "./transformation"; +import {processDpr} from "./transformationProcessing/processDpr"; @@ -31,6 +33,10 @@ export function generateTransformationString(transformationOptions: LegacyITrans return transformationOptions; } + if (transformationOptions instanceof Transformation){ + return transformationOptions.toString(); + } + if (Array.isArray(transformationOptions)) { return transformationOptions .map((singleTransformation) => { @@ -43,7 +49,6 @@ export function generateTransformationString(transformationOptions: LegacyITrans let width: string | number; let height: string | number; - const size = transformationOptions.size; const hasLayer = transformationOptions.overlay || transformationOptions.underlay; const crop = transformationOptions.crop; @@ -51,7 +56,7 @@ export function generateTransformationString(transformationOptions: LegacyITrans const background = (transformationOptions.background || '').replace(/^#/, "rgb:"); const color = (transformationOptions.color || '').replace(/^#/, "rgb:"); const flags = (toArray(transformationOptions.flags || [])).join('.'); - const dpr = transformationOptions.dpr; + const dpr = transformationOptions.dpr === undefined ? transformationOptions.dpr : processDpr(transformationOptions.dpr); const overlay = processLayer(transformationOptions.overlay); const radius = processRadius(transformationOptions.radius); diff --git a/src/backwards/legacyLayer/fetchlayer.ts b/src/backwards/legacyLayer/fetchlayer.ts new file mode 100644 index 00000000..9c9ffbe0 --- /dev/null +++ b/src/backwards/legacyLayer/fetchlayer.ts @@ -0,0 +1,38 @@ +import Layer from './layer'; +import {isString} from "../../internal/utils/dataStructureUtils"; +import {base64Encode} from "../../internal/utils/base64Encode"; + + +class FetchLayer extends Layer { + /** + * @class FetchLayer + * @classdesc Creates an image layer using a remote URL. + * @param {Object|string} options - layer parameters or a url + * @param {string} options.url the url of the image to fetch + */ + constructor(options:any) { + super(options); + if (isString(options)) { + this.options.url = options; + } else if (options != null ? options.url : void 0) { + this.options.url = options.url; + } + } + + url(url:string) { + this.options.url = url; + return this; + } + + /** + * generate the string representation of the layer + * @function FetchLayer#toString + * @return {String} + */ + toString() { + return `fetch:${base64Encode(this.options.url)}`; + } + +} + +export default FetchLayer; diff --git a/src/backwards/legacyLayer/layer.ts b/src/backwards/legacyLayer/layer.ts new file mode 100644 index 00000000..a0b7baa8 --- /dev/null +++ b/src/backwards/legacyLayer/layer.ts @@ -0,0 +1,108 @@ +import {snakeCase} from "../utils/snakeCase"; + +class Layer { + protected options: { + url?: any; + text?: string; + fontAntialiasing?: string; + fontHinting?: string; + lineSpacing?: string; + letterSpacing?: string; + stroke?: string; + textAlign?: string; + textDecoration?: string; + fontStyle?: string; + fontWeight?: string; + fontSize?: string | number; + fontFamily?: string; + format?: any; + publicId?: string; + type?: string; + resourceType?: string; + key?: string; + }; + /** + * Layer + * @constructor Layer + * @param {Object} options - layer parameters + */ + constructor(options?:{}) { + this.options = {}; + if (options != null) { + ["resourceType", "type", "publicId", "format"].forEach((key) => { + var ref; + // @ts-ignore + return this.options[key] = (ref = options[key]) != null ? ref : options[snakeCase(key)]; + }); + } + } + + resourceType(value:string) { + this.options.resourceType = value; + return this; + } + + type(value:string) { + this.options.type = value; + return this; + } + + publicId(value:string) { + this.options.publicId = value; + return this; + } + + /** + * Get the public ID, formatted for layer parameter + * @function Layer#getPublicId + * @return {String} public ID + */ + getPublicId() { + var ref; + return (ref = this.options.publicId) != null ? ref.replace(/\//g, ":") : void 0; + } + + /** + * Get the public ID, with format if present + * @function Layer#getFullPublicId + * @return {String} public ID + */ + getFullPublicId() { + if (this.options.format != null) { + return this.getPublicId() + "." + this.options.format; + } else { + return this.getPublicId(); + } + } + + format(value:any): this | void { + this.options.format = value; + return this; + } + + /** + * generate the string representation of the layer + * @function Layer#toString + */ + toString() { + let components:string[]=[]; + if (this.options.publicId == null) { + throw "Must supply publicId"; + } + if (!(this.options.resourceType === "image")) { + components.push(this.options.resourceType); + } + if (!(this.options.type === "upload")) { + components.push(this.options.type); + } + components.push(this.getFullPublicId()); + return components.filter(x => !!x).join(":"); + } + + clone() { + return new Layer(this.options); + } + +} + +export default Layer; diff --git a/src/backwards/legacyLayer/subtitleslayer.ts b/src/backwards/legacyLayer/subtitleslayer.ts new file mode 100644 index 00000000..6145294a --- /dev/null +++ b/src/backwards/legacyLayer/subtitleslayer.ts @@ -0,0 +1,15 @@ +import TextLayer from './textlayer'; + +class SubtitlesLayer extends TextLayer { + /** + * Represent a subtitles layer + * @constructor SubtitlesLayer + * @param {Object} options - layer parameters + */ + constructor(options: any) { + super(options); + this.options.resourceType = "subtitles"; + } + +} +export default SubtitlesLayer; diff --git a/src/backwards/legacyLayer/textlayer.ts b/src/backwards/legacyLayer/textlayer.ts new file mode 100644 index 00000000..82b83ba1 --- /dev/null +++ b/src/backwards/legacyLayer/textlayer.ts @@ -0,0 +1,176 @@ +import Layer from './layer'; +import {snakeCase} from "../utils/snakeCase"; +import {isEmpty} from "../utils/isEmpty"; +import {smartEscape} from "../utils/smartEscape"; +import {isNumberLike} from "../utils/isNumberLike"; + +class TextLayer extends Layer { + /** + * @constructor TextLayer + * @param {Object} options - layer parameters + */ + constructor(options?:{}) { + let keys; + super(options); + keys = ["resourceType", "resourceType", "fontFamily", "fontSize", "fontWeight", "fontStyle", "textDecoration", "textAlign", "stroke", "letterSpacing", "lineSpacing", "fontHinting", "fontAntialiasing", "text"]; + if (options != null) { + keys.forEach((key) => { + var ref; + // @ts-ignore + return this.options[key] = (ref = options[key]) != null ? ref : options[snakeCase(key)]; + }); + } + this.options.resourceType = "text"; + } + + //@ts-ignore + resourceType(resourceType:string) { + throw "Cannot modify resourceType for text layers"; + } + + //@ts-ignore + type(type:string) { + throw "Cannot modify type for text layers"; + } + + format(format:any) { + throw "Cannot modify format for text layers"; + } + + fontFamily(fontFamily:string) { + this.options.fontFamily = fontFamily; + return this; + } + + fontSize(fontSize:string | number) { + this.options.fontSize = fontSize; + return this; + } + + fontWeight(fontWeight:string) { + this.options.fontWeight = fontWeight; + return this; + } + + fontStyle(fontStyle:string) { + this.options.fontStyle = fontStyle; + return this; + } + + textDecoration(textDecoration:string) { + this.options.textDecoration = textDecoration; + return this; + } + + textAlign(textAlign:string) { + this.options.textAlign = textAlign; + return this; + } + + stroke(stroke:string) { + this.options.stroke = stroke; + return this; + } + + letterSpacing(letterSpacing:string) { + this.options.letterSpacing = letterSpacing; + return this; + } + + lineSpacing(lineSpacing:string) { + this.options.lineSpacing = lineSpacing; + return this; + } + + fontHinting (fontHinting:string){ + this.options.fontHinting = fontHinting; + return this; + } + + fontAntialiasing (fontAntialiasing:string){ + this.options.fontAntialiasing = fontAntialiasing; + return this; + } + + text(text:string) { + this.options.text = text; + return this; + } + + /** + * generate the string representation of the layer + * @function TextLayer#toString + * @return {String} + */ + toString() { + var components, hasPublicId, hasStyle, publicId, re, res, start, style, text, textSource; + style = this.textStyleIdentifier(); + if (this.options.publicId != null) { + publicId = this.getFullPublicId(); + } + if (this.options.text != null) { + hasPublicId = !isEmpty(publicId); + hasStyle = !isEmpty(style); + if (hasPublicId && hasStyle || !hasPublicId && !hasStyle) { + throw "Must supply either style parameters or a public_id when providing text parameter in a text overlay/underlay, but not both!"; + } + re = /\$\([a-zA-Z]\w*\)/g; + start = 0; + // textSource = text.replace(new RegExp("[,/]", 'g'), (c)-> "%#{c.charCodeAt(0).toString(16).toUpperCase()}") + textSource = smartEscape(this.options.text, /[,\/]/g); + text = ""; + while (res = re.exec(textSource)) { + text += smartEscape(textSource.slice(start, res.index)); + text += res[0]; + start = res.index + res[0].length; + } + text += smartEscape(textSource.slice(start)); + } + components = [this.options.resourceType, style, publicId, text]; + return (components).filter(x => !!x).join(":"); + } + + textStyleIdentifier() { + var components; + components = []; + if (this.options.fontWeight !== "normal") { + components.push(this.options.fontWeight); + } + if (this.options.fontStyle !== "normal") { + components.push(this.options.fontStyle); + } + if (this.options.textDecoration !== "none") { + components.push(this.options.textDecoration); + } + components.push(this.options.textAlign); + if (this.options.stroke !== "none") { + components.push(this.options.stroke); + } + if (!(isEmpty(this.options.letterSpacing) && !isNumberLike(this.options.letterSpacing))) { + components.push("letter_spacing_" + this.options.letterSpacing); + } + if (!(isEmpty(this.options.lineSpacing) && !isNumberLike(this.options.lineSpacing))) { + components.push("line_spacing_" + this.options.lineSpacing); + } + if (!(isEmpty(this.options.fontAntialiasing))) { + components.push("antialias_"+this.options.fontAntialiasing); + } + if (!(isEmpty(this.options.fontHinting))) { + components.push("hinting_"+this.options.fontHinting ); + } + if (!isEmpty(components.filter(x => !!x))) { + if (isEmpty(this.options.fontFamily)) { + throw `Must supply fontFamily. ${components}`; + } + if (isEmpty(this.options.fontSize) && !isNumberLike(this.options.fontSize)) { + throw "Must supply fontSize."; + } + } + components.unshift(this.options.fontFamily, this.options.fontSize); + components = components.filter(x => !!x).join("_"); + return components; + } + +}; + +export default TextLayer; diff --git a/src/backwards/parameters.ts b/src/backwards/parameters.ts new file mode 100644 index 00000000..3f9a6b11 --- /dev/null +++ b/src/backwards/parameters.ts @@ -0,0 +1,356 @@ +import Expression from './expression'; +import Transformation from "./transformation"; +import Layer from './legacyLayer/layer'; +import TextLayer from './legacyLayer/textlayer'; +import SubtitlesLayer from './legacyLayer/subtitleslayer'; +import FetchLayer from './legacyLayer/fetchlayer'; +import {isObject} from "./utils/isObject"; +import {isString} from "../internal/utils/dataStructureUtils"; +import {isEmpty} from "./utils/isEmpty"; +import {isFunction} from "./utils/isFunction"; +import {identity, withCamelCaseKeys} from "./utils/legacyBaseUtil"; + + +/** + * Return true if all items in list are strings + * @function Util.allString + * @param {Array} list - an array of items + */ +const allStrings = function(list:[]) { + return list.length && list.every(isString); +}; + +/** + * Transformation parameters + * Depends on 'util', 'transformation' + */ +class Param { + private name: string; + protected shortName: string; + protected process: (x: any) => any; + protected origValue: any; + /** + * Represents a single parameter. + * @class Param + * @param {string} name - The name of the parameter in snake_case + * @param {string} shortName - The name of the serialized form of the parameter. + * If a value is not provided, the parameter will not be serialized. + * @param {function} [process=Util.identity ] - Manipulate origValue when value is called + * @ignore + */ + constructor(name:string, shortName:string, process = identity) { + /** + * The name of the parameter in snake_case + * @member {string} Param#name + */ + this.name = name; + /** + * The name of the serialized form of the parameter + * @member {string} Param#shortName + */ + this.shortName = shortName; + /** + * Manipulate origValue when value is called + * @member {function} Param#process + */ + this.process = process; + } + + /** + * Set a (unprocessed) value for this parameter + * @function Param#set + * @param {*} origValue - the value of the parameter + * @return {Param} self for chaining + */ + set(origValue: any) { + this.origValue = origValue; + return this; + } + + /** + * Generate the serialized form of the parameter + * @function Param#serialize + * @return {string} the serialized form of the parameter + */ + serialize() { + var val, valid; + val = this.value(); + valid = Array.isArray(val) || isObject(val) || isString(val) ? !isEmpty(val) : val != null; + if ((this.shortName != null) && valid) { + return `${this.shortName}_${val}`; + } else { + return ''; + } + } + + /** + * Return the processed value of the parameter + * @function Param#value + */ + value() { + return this.process(this.origValue); + } + + static norm_color(value: string) { + return value != null ? value.replace(/^#/, 'rgb:') : void 0; + } + + static build_array(arg: any) { + if(arg == null) { + return []; + } else if (Array.isArray(arg)) { + return arg; + } else { + return [arg]; + } + } + + /** + * Covert value to video codec string. + * + * If the parameter is an object, + * @param {(string|Object)} param - the video codec as either a String or a Hash + * @return {string} the video codec string in the format codec:profile:level + * @example + * vc_[ :profile : [level]] + * or + { codec: 'h264', profile: 'basic', level: '3.1' } + * @ignore + */ + static process_video_params(param:any) { + var video; + switch (param.constructor) { + case Object: + video = ""; + if ('codec' in param) { + video = param.codec; + if ('profile' in param) { + video += ":" + param.profile; + if ('level' in param) { + video += ":" + param.level; + } + } + } + return video; + case String: + return param; + default: + return null; + } + } +} + +class ArrayParam extends Param { + private sep: string; + /** + * A parameter that represents an array. + * @param {string} name - The name of the parameter in snake_case. + * @param {string} shortName - The name of the serialized form of the parameter + * If a value is not provided, the parameter will not be serialized. + * @param {string} [sep='.'] - The separator to use when joining the array elements together + * @param {function} [process=Util.identity ] - Manipulate origValue when value is called + * @class ArrayParam + * @extends Param + * @ignore + */ + constructor(name:string, shortName:string, sep: string = '.', process: (x: any) => any = undefined) { + super(name, shortName, process); + this.sep = sep; + } + + serialize() { + if (this.shortName != null) { + let arrayValue = this.value(); + if (isEmpty(arrayValue)) { + return ''; + } else if (isString(arrayValue)) { + return `${this.shortName}_${arrayValue}`; + } else { + let flat = arrayValue.map((t: { serialize: () => any; })=>isFunction(t.serialize) ? t.serialize() : t).join(this.sep); + return `${this.shortName}_${flat}`; + } + } else { + return ''; + } + } + + value() { + if (Array.isArray(this.origValue)) { + return this.origValue.map(v=>this.process(v)); + } else { + return this.process(this.origValue); + } + } + + set(origValue: any) { + if ((origValue == null) || Array.isArray(origValue)) { + return super.set(origValue); + } else { + return super.set([origValue]); + } + } +} + +class TransformationParam extends Param { + private sep: string; + /** + * A parameter that represents a transformation + * @param {string} name - The name of the parameter in snake_case + * @param {string} [shortName='t'] - The name of the serialized form of the parameter + * @param {string} [sep='.'] - The separator to use when joining the array elements together + * @param {function} [process=Util.identity ] - Manipulate origValue when value is called + * @class TransformationParam + * @extends Param + * @ignore + */ + constructor(name:string, shortName = "t", sep = '.', process:(x: any) => any = undefined) { + super(name, shortName, process); + this.sep = sep; + } + + /** + * Generate string representations of the transformation. + * @returns {*} Returns either the transformation as a string, or an array of string representations. + */ + serialize() { + let result = ''; + const val = this.value(); + + if (isEmpty(val)) { + return result; + } + + // val is an array of strings so join them + if (allStrings(val)) { + const joined = val.join(this.sep); // creates t1.t2.t3 in case multiple named transformations were configured + if (!isEmpty(joined)) { + // in case options.transformation was not set with an empty string (val != ['']); + result = `${this.shortName}_${joined}`; + } + } else { // Convert val to an array of strings + result = val.map((t: { serialize: () => any; }) => { + if (isString(t) && !isEmpty(t)) { + return `${this.shortName}_${t}`; + } + if (isFunction(t.serialize)) { + return t.serialize(); + } + if (isObject(t) && !isEmpty(t)) { + return new Transformation(t).serialize(); + } + return undefined; + }).filter((t: any)=>t); + } + return result; + } + + set(origValue1: any[]) { + this.origValue = origValue1; + if (Array.isArray(this.origValue)) { + return super.set(this.origValue); + } else { + return super.set([this.origValue]); + } + } +} + +const number_pattern = "([0-9]*)\\.([0-9]+)|([0-9]+)"; +const offset_any_pattern = "(" + number_pattern + ")([%pP])?"; + +class RangeParam extends Param { + + /** + * A parameter that represents a range + * @param {string} name - The name of the parameter in snake_case + * @param {string} shortName - The name of the serialized form of the parameter + * If a value is not provided, the parameter will not be serialized. + * @param {function} [process=norm_range_value ] - Manipulate origValue when value is called + * @class RangeParam + * @extends Param + * @ignore + */ + constructor(name:string, shortName:string, process = RangeParam.norm_range_value) { + super(name, shortName, process); + } + static norm_range_value(value: string) { + + let offset = String(value).match(new RegExp('^' + offset_any_pattern + '$')); + if (offset) { + let modifier = offset[5] != null ? 'p' : ''; + value = (offset[1] || offset[4]) + modifier; + } + return value; + } +} + +class RawParam extends Param { + constructor(name: string, shortName: string, process = identity) { + super(name, shortName, process); + } + + serialize() { + return this.value(); + } + +} + +class LayerParam extends Param { + // Parse layer options + // @return [string] layer transformation string + // @private + value() { + if (this.origValue == null) { + return ''; + } + let result; + if (this.origValue instanceof Layer) { + result = this.origValue; + } else if (isObject(this.origValue)) { + let layerOptions = withCamelCaseKeys(this.origValue); + // @ts-ignore + if (layerOptions.resourceType === "text" || (layerOptions.text != null)) { + result = new TextLayer(layerOptions); + } else { // @ts-ignore + if (layerOptions.resourceType === "subtitles") { + result = new SubtitlesLayer(layerOptions); + } else { // @ts-ignore + if (layerOptions.resourceType === "fetch" || (layerOptions.url != null)) { + result = new FetchLayer(layerOptions); + } else { + result = new Layer(layerOptions); + } + } + } + } else if (isString(this.origValue)) { + if (/^fetch:.+/.test(this.origValue)) { + result = new FetchLayer(this.origValue.substr(6)); + } else { + result = this.origValue; + } + } else { + result = ''; + } + return result.toString(); + } + + static textStyle(layer: { key?: any; }) { + return (new TextLayer(layer)).textStyleIdentifier(); + } +} + +class ExpressionParam extends Param { + // @ts-ignore + serialize() { + return Expression.normalize(super.serialize()); + } +} + +export { + Param, + ArrayParam, + TransformationParam, + RangeParam, + RawParam, + LayerParam, + ExpressionParam +}; diff --git a/src/backwards/transformation.ts b/src/backwards/transformation.ts new file mode 100644 index 00000000..f7a4f9a3 --- /dev/null +++ b/src/backwards/transformation.ts @@ -0,0 +1,1060 @@ +import Expression from './expression'; +import Condition from './condition'; +import {CONFIG_PARAMS} from './configuration'; +import cloneDeep from 'lodash.clonedeep'; + +/** + * A list of keys used by the url() function. + * @private + */ +export const URL_KEYS = [ + 'accessibility', + 'api_secret', + 'auth_token', + 'cdn_subdomain', + 'cloud_name', + 'cname', + 'format', + 'placeholder', + 'private_cdn', + 'resource_type', + 'secure', + 'secure_cdn_subdomain', + 'secure_distribution', + 'shorten', + 'sign_url', + 'signature', + 'ssl_detected', + 'type', + 'url_suffix', + 'use_root_path', + 'version' +]; + +import { + Param, + ArrayParam, + LayerParam, + RangeParam, + RawParam, + TransformationParam +} from "./parameters"; +import {isEmpty} from "./utils/isEmpty"; +import {isFunction} from "./utils/isFunction"; +import {camelCase, contains, difference, identity} from "./utils/legacyBaseUtil"; +import {snakeCase} from "./utils/snakeCase"; +import {isObject} from "./utils/isObject"; +import {isString} from "../internal/utils/dataStructureUtils"; +import Layer from "./legacyLayer/layer"; +import {stringOrNumber} from "../types/types"; + +/** + * Assign key, value to target, when value is not null.
+ * This function mutates the target! + * @param {object} target the object to assign the values to + * @param {object} sources one or more objects to get values from + * @returns {object} the target after the assignment + */ +function assignNotNull(target:{}, ...sources:object[]) { + sources.forEach(source => { + Object.keys(source).forEach(key => { + // @ts-ignore + if (source[key] != null) { + // @ts-ignore + target[key] = source[key]; + } + }); + }); + return target; +} + +/** + * TransformationBase + * Depends on 'configuration', 'parameters','util' + * @internal + */ + +class TransformationBase { + private toOptions: any; + private otherOptions: any; + protected chained: any; + private setParent: (arg0: object) => this; + private getParent: () => this; + protected param?: (value?: any, name?: any, abbr?: any, defaultValue?: any, process?: any) => this; + protected rawParam?: (value?: any, name?: any, abbr?: any, defaultValue?: any, process?: any) => TransformationBase; + protected rangeParam?: (value?: any, name?: any, abbr?: any, defaultValue?: any, process?: any) => TransformationBase; + protected arrayParam: (value?: any, name?: any, abbr?: any, sep?: string, defaultValue?: any, process?: any) => TransformationBase; + protected transformationParam: (value?: any, name?: any, abbr?: any, sep?: string, defaultValue?: any, process?: any) => TransformationBase; + protected layerParam: (value?: any, name?: any, abbr?: any) => TransformationBase; + protected getValue: (name: any) => any; + protected get: (name: any) => any; + private remove: (name: any) => (any | null); + private keys: () => any[]; + private toPlainObject: () => {} | { transformation: any }; + private resetTransformations: () => TransformationBase; + chain: () => Transformation; + + /** + * The base class for transformations. + * Members of this class are documented as belonging to the {@link Transformation} class for convenience. + * @class TransformationBase + */ + constructor(options:any) { + /** @private */ + /** @private */ + let parent:any; + let trans: {name?:Param|RawParam|RangeParam}; + parent = void 0; + trans = {}; + /** + * Return an options object that can be used to create an identical Transformation + * @function Transformation#toOptions + * @return {Object} Returns a plain object representing this transformation + */ + this.toOptions = (withChain:any) => { + let opt = {}; + if(withChain == null) { + withChain = true; + } + // @ts-ignore + Object.keys(trans).forEach(key => opt[key] = trans[key].origValue); + assignNotNull(opt, this.otherOptions); + if (withChain && !isEmpty(this.chained)) { + let list = this.chained.map((tr: { toOptions: () => any; }) => tr.toOptions()); + list.push(opt); + opt = {}; + assignNotNull(opt, this.otherOptions); + // @ts-ignore + opt.transformation = list; + } + return opt; + }; + /** + * Set a parent for this object for chaining purposes. + * + * @function Transformation#setParent + * @param {Object} object - the parent to be assigned to + * @returns {Transformation} Returns this instance for chaining purposes. + */ + this.setParent = (object) => { + parent = object; + if (object != null) { + // @ts-ignore + this.fromOptions(typeof object.toOptions === "function" ? object.toOptions() : void 0); + } + return this; + }; + /** + * Returns the parent of this object in the chain + * @function Transformation#getParent + * @protected + * @return {Object} Returns the parent of this object if there is any + */ + this.getParent = () => { + return parent; + }; + + // Helper methods to create parameter methods + // These methods are defined here because they access `trans` which is + // a private member of `TransformationBase` + + /** @protected */ + this.param = (value, name, abbr, defaultValue, process) => { + if (process == null) { + if (isFunction(defaultValue)) { + process = defaultValue; + } else { + process = identity; + } + } + // @ts-ignore + trans[name] = new Param(name, abbr, process).set(value); + return this; + }; + /** @protected */ + this.rawParam = function (value, name, abbr, defaultValue, process) { + process = lastArgCallback(arguments); + // @ts-ignore + trans[name] = new RawParam(name, abbr, process).set(value); + return this; + }; + /** @protected */ + this.rangeParam = function (value, name, abbr, defaultValue, process) { + process = lastArgCallback(arguments); + // @ts-ignore + trans[name] = new RangeParam(name, abbr, process).set(value); + return this; + }; + /** @protected */ + this.arrayParam = function (value, name, abbr, sep = ":", defaultValue = [], process = undefined) { + process = lastArgCallback(arguments); + // @ts-ignore + trans[name] = new ArrayParam(name, abbr, sep, process).set(value); + return this; + }; + /** @protected */ + this.transformationParam = function (value, name, abbr, sep = ".", defaultValue = undefined, process = undefined) { + process = lastArgCallback(arguments); + // @ts-ignore + trans[name] = new TransformationParam(name, abbr, sep, process).set(value); + return this; + }; + this.layerParam = function (value, name, abbr) { + // @ts-ignore + trans[name] = new LayerParam(name, abbr).set(value); + return this; + }; + + // End Helper methods + + /** + * Get the value associated with the given name. + * Get the value associated with the given name. + * @function Transformation#getValue + * @param {string} name - the name of the parameter + * @return {*} the processed value associated with the given name + * @description Use {@link get}.origValue for the value originally provided for the parameter + */ + this.getValue = function (name) { + // @ts-ignore + let value = trans[name] && trans[name].value(); + return value != null ? value : this.otherOptions[name]; + }; + /** + * Get the parameter object for the given parameter name + * @function Transformation#get + * @param {string} name the name of the transformation parameter + * @returns {Param} the param object for the given name, or undefined + */ + this.get = function (name) { + // @ts-ignore + return trans[name]; + }; + /** + * Remove a transformation option from the transformation. + * @function Transformation#remove + * @param {string} name - the name of the option to remove + * @return {*} Returns the option that was removed or null if no option by that name was found. The type of the + * returned value depends on the value. + */ + this.remove = function (name) { + var temp; + switch (false) { + // @ts-ignore + case trans[name] == null: + // @ts-ignore + temp = trans[name]; + // @ts-ignore + delete trans[name]; + return temp.origValue; + case this.otherOptions[name] == null: + temp = this.otherOptions[name]; + delete this.otherOptions[name]; + return temp; + default: + return null; + } + }; + /** + * Return an array of all the keys (option names) in the transformation. + * @return {Array} the keys in snakeCase format + */ + this.keys = function () { + var key; + return ((function () { + var results; + results = []; + for (key in trans) { + if (key != null) { + results.push(key.match(VAR_NAME_RE) ? key : snakeCase(key)); + } + } + return results; + })()).sort(); + }; + /** + * Returns a plain object representation of the transformation. Values are processed. + * @function Transformation#toPlainObject + * @return {Object} the transformation options as plain object + */ + this.toPlainObject = function () { + var hash, key, list; + hash = {}; + for (key in trans) { + // @ts-ignore + hash[key] = trans[key].value(); + // @ts-ignore + if (isObject(hash[key])) { + // @ts-ignore + hash[key] = cloneDeep(hash[key]); + } + } + if (!isEmpty(this.chained)) { + list = this.chained.map((tr: { toPlainObject: () => any; }) => tr.toPlainObject()); + list.push(hash); + hash = { + transformation: list + }; + } + return hash; + }; + /** + * Complete the current transformation and chain to a new one. + * In the URL, transformations are chained together by slashes. + * @function Transformation#chain + * @return {Transformation} Returns this transformation for chaining + * @example + * var tr = cloudinary.Transformation.new(); + * tr.width(10).crop('fit').chain().angle(15).serialize() + * // produces "c_fit,w_10/a_15" + */ + this.chain = function () { + var names, tr; + names = Object.getOwnPropertyNames(trans); + if (names.length !== 0) { + tr = new this.constructor(this.toOptions(false)); + this.resetTransformations(); + this.chained.push(tr); + } + return this; + }; + this.resetTransformations = function () { + trans = {}; + return this; + }; + this.otherOptions = {}; + this.chained = []; + this.fromOptions(options); + } + + /** + * Merge the provided options with own's options + * @param {Object} [options={}] key-value list of options + * @returns {Transformation} Returns this instance for chaining + */ + fromOptions(options = {}) { + if (options instanceof TransformationBase) { + this.fromTransformation(options); + } else { + if (isString(options) || Array.isArray(options)) { + options = { + transformation: options + }; + } + options = cloneDeep(options); + // Handling of "if" statements precedes other options as it creates a chained transformation + // @ts-ignore + if (options["if"]) { + // @ts-ignore + this.set("if", options["if"]); + // @ts-ignore + delete options["if"]; + } + for (let key in options) { + // @ts-ignore + let opt = options[key]; + if(opt != null) { + if (key.match(VAR_NAME_RE)) { + if (key !== '$attr') { + this.set('variable', key, opt); + } + } else { + this.set(key, opt); + } + } + } + } + return this; + } + + fromTransformation(other:any) { + if (other instanceof TransformationBase) { + other.keys().forEach(key => + this.set(key, other.get(key).origValue) + ); + } + return this; + } + + /** + * Set a parameter. + * The parameter name `key` is converted to + * @param {string} key - the name of the parameter + * @param {*} values - the value of the parameter + * @returns {Transformation} Returns this instance for chaining + */ + set(key:string, ...values: string[]) { + let camelKey; + camelKey = camelCase(key); + if (contains(methods, camelKey)) { + // @ts-ignore + this[camelKey].apply(this, values); + } else { + this.otherOptions[key] = values[0]; + } + return this; + } + + hasLayer() { + return this.getValue("overlay") || this.getValue("underlay"); + } + + /** + * Generate a string representation of the transformation. + * @function Transformation#serialize + * @return {string} Returns the transformation as a string + */ + serialize() { + var ifParam, j, len, paramList, ref, ref1, ref2, ref3, ref4, resultArray, t, transformationList, + transformationString, transformations, value, variables, vars; + resultArray = this.chained.map((tr: { serialize?: () => any; }) => tr.serialize()); + paramList = this.keys(); + transformations = (ref = this.get("transformation")) != null ? ref.serialize() : void 0; + ifParam = (ref1 = this.get("if")) != null ? ref1.serialize() : void 0; + variables = processVar((ref2 = this.get("variables")) != null ? ref2.value() : void 0); + paramList = difference(paramList, ["transformation", "if", "variables"]); + vars = []; + transformationList = []; + for (j = 0, len = paramList.length; j < len; j++) { + t = paramList[j]; + if (t.match(VAR_NAME_RE)) { + vars.push(t + "_" + Expression.normalize((ref3 = this.get(t)) != null ? ref3.value() : void 0)); + } else { + transformationList.push((ref4 = this.get(t)) != null ? ref4.serialize() : void 0); + } + } + switch (false) { + case !isString(transformations): + transformationList.push(transformations); + break; + case !Array.isArray(transformations): + resultArray = resultArray.concat(transformations); + } + transformationList = (function () { + var k, len1, results; + results = []; + for (k = 0, len1 = transformationList.length; k < len1; k++) { + value = transformationList[k]; + if (Array.isArray(value) && !isEmpty(value) || !Array.isArray(value) && value) { + results.push(value); + } + } + return results; + })(); + transformationList = vars.sort().concat(variables).concat(transformationList.sort()); + if (ifParam === "if_end") { + transformationList.push(ifParam); + } else if (!isEmpty(ifParam)) { + transformationList.unshift(ifParam); + } + transformationString = (transformationList).filter(x => !!x).join(param_separator); + if (!isEmpty(transformationString)) { + resultArray.push(transformationString); + } + return (resultArray).filter((x: any) => !!x).join(trans_separator); + } + + /** + * Provide a list of all the valid transformation option names + * @function Transformation#listNames + * @private + * @return {Array} a array of all the valid option names + */ + static listNames() { + return methods; + } + + /** + * Returns the attributes for an HTML tag. + * @function Cloudinary.toHtmlAttributes + * @return PlainObject + */ + toHtmlAttributes() { + let attrName, height, options:any, ref2, ref3, value, width; + options = {}; + let snakeCaseKey; + Object.keys(this.otherOptions).forEach(key=>{ + value = this.otherOptions[key]; + snakeCaseKey = snakeCase(key); + if (!contains(PARAM_NAMES, snakeCaseKey) && !contains(URL_KEYS, snakeCaseKey)) { + attrName = /^html_/.test(key) ? key.slice(5) : key; + options[attrName] = value; + } + }); + // convert all "html_key" to "key" with the same value + this.keys().forEach(key => { + if (/^html_/.test(key)) { + options[camelCase(key.slice(5))] = this.getValue(key); + } + }); + if (!(this.hasLayer() || this.getValue("angle") || contains(["fit", "limit", "lfill"], this.getValue("crop")))) { + width = (ref2 = this.get("width")) != null ? ref2.origValue : void 0; + height = (ref3 = this.get("height")) != null ? ref3.origValue : void 0; + if (parseFloat(width) >= 1.0) { + if (options.width == null) { + options.width = width; + } + } + if (parseFloat(height) >= 1.0) { + if (options.height == null) { + options.height = height; + } + } + } + return options; + } + + static isValidParamName(name: string) { + return methods.indexOf(camelCase(name)) >= 0; + } + + /** + * Delegate to the parent (up the call chain) to produce HTML + * @function Transformation#toHtml + * @return {string} HTML representation of the parent if possible. + * @example + * tag = cloudinary.ImageTag.new("sample", {cloud_name: "demo"}) + * // ImageTag {name: "img", publicId: "sample"} + * tag.toHtml() + * // + * tag.transformation().crop("fit").width(300).toHtml() + * // + */ + toHtml():any { + var ref; + return (ref = this.getParent()) != null ? typeof ref.toHtml === "function" ? ref.toHtml() : void 0 : void 0; + } + + toString() { + return this.serialize(); + } + + clone() { + return new TransformationBase(this.toOptions(true)); + } +} + +const VAR_NAME_RE = /^\$[a-zA-Z0-9]+$/; + +const trans_separator = '/'; + +const param_separator = ','; + + +function lastArgCallback(args: string | IArguments | any[]) { + var callback; + callback = args != null ? args[args.length - 1] : void 0; + if (isFunction(callback)) { + return callback; + } else { + return void 0; + } +} + +function processVar(varArray: string | any[]) { + var j, len, name, results, v; + if (Array.isArray(varArray)) { + results = []; + for (j = 0, len = varArray.length; j < len; j++) { + [name, v] = varArray[j]; + results.push(`${name}_${Expression.normalize(v)}`); + } + return results; + } else { + return varArray; + } +} + +// @ts-ignore +function processCustomFunction({function_type, source}) { + if (function_type === 'remote') { + return [function_type, btoa(source)].join(":"); + } else if (function_type === 'wasm') { + return [function_type, source].join(":"); + } +} + +/** + * Transformation Class methods. + * This is a list of the parameters defined in Transformation. + * Values are camelCased. + * @const Transformation.methods + * @private + * @ignore + * @type {Array} + */ +/** + * Parameters that are filtered out before passing the options to an HTML tag. + * + * The list of parameters is a combination of `Transformation::methods` and `Configuration::CONFIG_PARAMS` + * @const {Array} Transformation.PARAM_NAMES + * @private + * @ignore + * @see toHtmlAttributes + */ +class Transformation extends TransformationBase { + /** + * Represents a single transformation. + * @class Transformation + * @example + * t = new cloudinary.Transformation(); + * t.angle(20).crop("scale").width("auto"); + * + * // or + * + * t = new cloudinary.Transformation( {angle: 20, crop: "scale", width: "auto"}); + * @see Available image transformations + * @see Available video transformations + */ + constructor(options?: {}) { + super(options); + } + + /** + * Convenience constructor + * @param {Object} options + * @return {Transformation} + * @example cl = cloudinary.Transformation.new( {angle: 20, crop: "scale", width: "auto"}) + */ + static new(options?: { serialize?: () => any; }) { + return new Transformation(options); + } + + /* + Transformation Parameters + */ + angle(value: string | number) { + this.arrayParam(value, "angle", "a", ".", Expression.normalize); + return this; + } + + audioCodec(value: string | number) { + this.param(value, "audio_codec", "ac"); + return this; + } + + audioFrequency(value: string | number) { + this.param(value, "audio_frequency", "af"); + return this; + } + + aspectRatio(value: string | number) { + this.param(value, "aspect_ratio", "ar", Expression.normalize); + return this; + } + + background(value: string | number) { + this.param(value, "background", "b", Param.norm_color); + return this; + } + + bitRate(value: string | number) { + this.param(value, "bit_rate", "br"); + return this; + } + + border(value: string | number) { + return this.param(value, "border", "bo", (border:any) => { + if (isObject(border)) { + border = Object.assign({}, { + color: "black", + width: 2 + }, border); + return `${border.width}px_solid_${Param.norm_color(border.color)}`; + } else { + return border; + } + }); + } + + color(value: string | number) { + this.param(value, "color", "co", Param.norm_color); + return this; + } + + colorSpace(value: string | number) { + this.param(value, "color_space", "cs"); + return this; + } + + crop(value: string | number) { + this.param(value, "crop", "c"); + return this; + } + + customFunction(value: any) { + return this.param(value, "custom_function", "fn", () => { + return processCustomFunction(value); + }); + } + + customPreFunction(value: any) { + if (this.get('custom_function')) { + return; + } + return this.rawParam(value, "custom_function", "", () => { + value = processCustomFunction(value); + return value ? `fn_pre:${value}` : value; + }); + } + + defaultImage(value: string) { + this.param(value, "default_image", "d"); + return this; + } + + delay(value: string | number) { + this.param(value, "delay", "dl"); + return this; + } + + density(value: string | number) { + this.param(value, "density", "dn"); + return this; + } + + duration(value: string | number) { + this.rangeParam(value, "duration", "du"); + return this; + } + + dpr(value: string | number) { + return this.param(value, "dpr", "dpr", (dpr: string) => { + dpr = dpr.toString(); + if (dpr != null ? dpr.match(/^\d+$/) : void 0) { + return dpr + ".0"; + } else { + return Expression.normalize(dpr); + } + }); + } + + effect(value: string | Array) { + this.arrayParam(value, "effect", "e", ":", Expression.normalize); + return this; + } + + else() { + return this.if('else'); + } + + endIf() { + return this.if('end'); + } + + endOffset(value: string | number) { + this.rangeParam(value, "end_offset", "eo"); + return this; + } + + fallbackContent(value: string) { + this.param(value, "fallback_content"); + return this; + } + + fetchFormat(value: string) { + this.param(value, "fetch_format", "f"); + return this; + } + + format(value: string) { + this.param(value, "format"); + return this; + } + + flags(value: string) { + this.arrayParam(value, "flags", "fl", "."); + return this; + } + + gravity(value: any) { + this.param(value, "gravity", "g"); + return this; + } + + fps(value: string | Array) { + return this.param(value, "fps", "fps", (fps: any[]) => { + if (isString(fps)) { + return fps; + } else if (Array.isArray(fps)) { + return fps.join("-"); + } else { + return fps; + } + }); + } + + height(value:string | number) { + return this.param(value, "height", "h", () => { + if (this.getValue("crop") || this.getValue("overlay") || this.getValue("underlay")) { + return Expression.normalize(value); + } else { + return null; + } + }); + } + + htmlHeight(value:string) { + this.param(value, "html_height"); + return this; + } + + htmlWidth(value:string) { + this.param(value, "html_width"); + return this; + } + + if(value = "") { + var i, ifVal, j, ref, trIf, trRest; + switch (value) { + case "else": + this.chain(); + return this.param(value, "if", "if"); + case "end": + this.chain(); + for (i = j = ref = this.chained.length - 1; j >= 0; i = j += -1) { + ifVal = this.chained[i].getValue("if"); + if (ifVal === "end") { + break; + } else if (ifVal != null) { + trIf = Transformation.new().if(ifVal); + this.chained[i].remove("if"); + trRest = this.chained[i]; + this.chained[i] = Transformation.new().transformation([trIf, trRest]); + if (ifVal !== "else") { + break; + } + } + } + return this.param(value, "if", "if"); + case "": + return Condition.new().setParent(this); + default: + return this.param(value, "if", "if", (value:any) => { + return Condition.new(value).toString(); + }); + } + } + + keyframeInterval(value:number) { + this.param(value, "keyframe_interval", "ki"); + return this; + } + + ocr(value:any) { + this.param(value, "ocr", "ocr"); + return this; + } + + offset(value:any) { + var end_o, start_o; + [start_o, end_o] = (isFunction(value != null ? value.split : void 0)) ? value.split('..') : Array.isArray(value) ? value : [null, null]; + if (start_o != null) { + this.startOffset(start_o); + } + if (end_o != null) { + return this.endOffset(end_o); + } + } + + opacity(value: number) { + this.param(value, "opacity", "o", Expression.normalize); + return this; + } + + overlay(value: string | object) { + this.layerParam(value, "overlay", "l"); + return this; + } + + page(value: number) { + this.param(value, "page", "pg"); + return this; + } + + poster(value: string | object) { + this.param(value, "poster"); + return this; + } + + prefix(value: string) { + this.param(value, "prefix", "p"); + return this; + } + + quality(value: string | number) { + this.param(value, "quality", "q", Expression.normalize); + return this; + } + + radius(value: "max" | number) { + this.arrayParam(value, "radius", "r", ":", Expression.normalize); + return this; + } + + rawTransformation(value: any) { + this.rawParam(value, "raw_transformation"); + return this; + } + + size(value: string) { + let height, width; + if (isFunction(value != null ? value.split : void 0)) { + [width, height] = value.split('x'); + this.width(width); + return this.height(height); + } + } + + sourceTypes(value: object) { + this.param(value, "source_types"); + return this; + } + + sourceTransformation(value: any) { + return this.param(value, "source_transformation"); + } + + startOffset(value:string | number) { + this.rangeParam(value, "start_offset", "so"); + return this; + } + + streamingProfile(value:string) { + this.param(value, "streaming_profile", "sp"); + return this; + } + + transformation(value:any) { + this.transformationParam(value, "transformation", "t"); + return this; + } + + underlay(value: string) { + this.layerParam(value, "underlay", "u"); + return this; + } + + variable(name: string, value: any) { + this.param(value, name, name); + return this; + } + + variables(values: Array<[string, any]>) { + this.arrayParam(values, "variables"); + return this; + } + + videoCodec(value:string | number | Object) { + this.param(value, "video_codec", "vc", Param.process_video_params); + return this; + } + + videoSampling(value:string | number) { + this.param(value, "video_sampling", "vs"); + return this; + } + + width(value:string | number) { + this.param(value, "width", "w", () => { + if (this.getValue("crop") || this.getValue("overlay") || this.getValue("underlay")) { + return Expression.normalize(value); + } else { + return null; + } + }); + return this; + } + + x(value:number) { + this.param(value, "x", "x", Expression.normalize); + return this; + } + + y(value:number) { + this.param(value, "y", "y", Expression.normalize); + return this; + } + + zoom(value:number | string) { + this.param(value, "zoom", "z", Expression.normalize); + return this; + } + +} + +/** + * Transformation Class methods. + * This is a list of the parameters defined in Transformation. + * Values are camelCased. + */ +const methods = [ + "angle", + "audioCodec", + "audioFrequency", + "aspectRatio", + "background", + "bitRate", + "border", + "color", + "colorSpace", + "crop", + "customFunction", + "customPreFunction", + "defaultImage", + "delay", + "density", + "duration", + "dpr", + "effect", + "else", + "endIf", + "endOffset", + "fallbackContent", + "fetchFormat", + "format", + "flags", + "gravity", + "fps", + "height", + "htmlHeight", + "htmlWidth", + "if", + "keyframeInterval", + "ocr", + "offset", + "opacity", + "overlay", + "page", + "poster", + "prefix", + "quality", + "radius", + "rawTransformation", + "size", + "sourceTypes", + "sourceTransformation", + "startOffset", + "streamingProfile", + "transformation", + "underlay", + "variable", + "variables", + "videoCodec", + "videoSampling", + "width", + "x", + "y", + "zoom" +]; + +/** + * Parameters that are filtered out before passing the options to an HTML tag. + * + * The list of parameters is a combination of `Transformation::methods` and `Configuration::CONFIG_PARAMS` + */ +const PARAM_NAMES = methods.map(snakeCase).concat(CONFIG_PARAMS); + +export default Transformation; diff --git a/src/backwards/transformationProcessing/processDpr.ts b/src/backwards/transformationProcessing/processDpr.ts new file mode 100644 index 00000000..a2e45a91 --- /dev/null +++ b/src/backwards/transformationProcessing/processDpr.ts @@ -0,0 +1,14 @@ +import Expression from "../expression"; + +/** + * Process DPR value. If input is 1 returns 1.0 + * @param value + */ +export function processDpr(value: string | number) { + let dpr = value.toString(); + if (dpr != null ? dpr.match(/^\d+$/) : void 0) { + return dpr + ".0"; + } else { + return Expression.normalize(dpr); + } +} diff --git a/src/backwards/transformationProcessing/processLayer.ts b/src/backwards/transformationProcessing/processLayer.ts index 5ccd223b..961dd9d7 100644 --- a/src/backwards/transformationProcessing/processLayer.ts +++ b/src/backwards/transformationProcessing/processLayer.ts @@ -8,6 +8,8 @@ import {isObject} from "../utils/isObject"; import {base64Encode} from "../../internal/utils/base64Encode"; import {LAYER_KEYWORD_PARAMS} from "../consts"; import {smartEscape} from "../utils/smartEscape"; +import TextLayer from "../legacyLayer/textlayer"; +import Layer from "../legacyLayer/layer"; export function textStyle(layer: any) { const keywords:any[] = []; @@ -44,6 +46,9 @@ export function textStyle(layer: any) { export function processLayer(layer: any) { + if (layer instanceof TextLayer || layer instanceof Layer){ + return layer.toString(); + } let result = ''; if (isObject(layer)) { if (layer.resource_type === "fetch" || (layer.url != null)) { diff --git a/src/backwards/utils/isEmpty.ts b/src/backwards/utils/isEmpty.ts new file mode 100644 index 00000000..63d577b3 --- /dev/null +++ b/src/backwards/utils/isEmpty.ts @@ -0,0 +1,6 @@ +export function isEmpty(value:any){ + return value === undefined || + value === null || + (typeof value === "object" && Object.keys(value).length === 0) || + (typeof value === "string" && value.trim().length === 0) +} diff --git a/src/backwards/utils/isFunction.ts b/src/backwards/utils/isFunction.ts new file mode 100644 index 00000000..958c663a --- /dev/null +++ b/src/backwards/utils/isFunction.ts @@ -0,0 +1,9 @@ +/** + * Simple Is Function check + * + * @param variableToCheck + * @returns {boolean} + */ +export function isFunction(variableToCheck: any) { + return variableToCheck instanceof Function; +} diff --git a/src/backwards/utils/isNumberLike.ts b/src/backwards/utils/isNumberLike.ts new file mode 100644 index 00000000..66c125d9 --- /dev/null +++ b/src/backwards/utils/isNumberLike.ts @@ -0,0 +1,14 @@ +/** + * Return true is value is a number or a string representation of a number. + * @function Util.isNumberLike + * @param {*} value + * @returns {boolean} true if value is a number + * @example + * Util.isNumber(0) // true + * Util.isNumber("1.3") // true + * Util.isNumber("") // false + * Util.isNumber(undefined) // false + */ +export const isNumberLike = function(value: any) { + return (value != null) && !isNaN(parseFloat(value)); +}; diff --git a/src/backwards/utils/legacyBaseUtil.ts b/src/backwards/utils/legacyBaseUtil.ts new file mode 100644 index 00000000..29da21ae --- /dev/null +++ b/src/backwards/utils/legacyBaseUtil.ts @@ -0,0 +1,74 @@ +/** + * Create a copy of the source object with all keys in camelCase + * @function Util.withCamelCaseKeys + * @return {Object} a new object + * @param source + */ +import {isEmpty} from "./isEmpty"; + +export const withCamelCaseKeys = function(source: {}) { + return convertKeys(source, camelCase); +}; + + +/** + * Convert string to camelCase + * @function Util.camelCase + * @param {string} source - the string to convert + * @return {string} in camelCase format + */ +export const camelCase = function(source: string) { + var words = source.match(reWords); + words = words.map(word=> word.charAt(0).toLocaleUpperCase() + word.slice(1).toLocaleLowerCase()); + words[0] = words[0].toLocaleLowerCase(); + + return words.join(''); +}; + + +/** + * Creates a new object from source, with the keys transformed using the converter. + * @param {object} source + * @param {function|null} converter + * @returns {object} + */ +export var convertKeys = function(source: { [x: string]: any; }, converter: { (source: string): string; (arg0: string): string; }) { + var result, value; + result = {}; + for (let key in source) { + value = source[key]; + if(converter) { + key = converter(key); + } + if (!isEmpty(key)) { + // @ts-ignore + result[key] = value; + } + } + return result; +}; + +export var reWords = (function() { + var lower, upper; + upper = '[A-Z]'; + lower = '[a-z]+'; + return RegExp(upper + '+(?=' + upper + lower + ')|' + upper + '?' + lower + '|' + upper + '+|[0-9]+', 'g'); +})(); + +export function identity(x:any) { + return x; +} + +export function contains(a: string | any[], obj: any) { + for (let i = 0; i < a.length; i++) { + if (a[i] === obj) { + return true; + } + } + return false; +} + +export function difference(arr1:any[], arr2:any[]){ + return arr1.filter(x => !arr2.includes(x)); +} + diff --git a/src/backwards/utils/snakeCase.ts b/src/backwards/utils/snakeCase.ts new file mode 100644 index 00000000..7eb06eaf --- /dev/null +++ b/src/backwards/utils/snakeCase.ts @@ -0,0 +1,5 @@ +/** + * Converts string to snake case + * @param {string} str + */ +export const snakeCase = (str:string) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); diff --git a/src/types/types.ts b/src/types/types.ts index b056be73..13f00914 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,3 +1,5 @@ +import Transformation from "../backwards/transformation"; + export type StreamingProfileTypes = string | "4k" | "full_hd" | "hd" | "sd" | "full_hd_wifi" | "full_hd_lean" | "hd_lean"; export type stringOrNumber = number | string; @@ -400,7 +402,7 @@ export type SimulateColorBlindType = "tritanopia"; export interface LegacyITransforamtionOptions { - transformation?: LegacyITransforamtionOptions | string; + transformation?: LegacyITransforamtionOptions | string | Transformation; raw_transformation?: string; crop?: CropMode; width?: stringOrNumber; From 3c0968b6e23f021bb3c000599f288e6598e4fb16 Mon Sep 17 00:00:00 2001 From: Raya Straus Date: Sun, 15 Aug 2021 10:39:39 +0300 Subject: [PATCH 2/3] increases size limit --- __TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts b/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts index ab0e2b7f..ca62cd5e 100644 --- a/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts +++ b/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts @@ -67,7 +67,7 @@ const bundleSizeTestCases:ITestCase[] = [ }, { name: 'Import backwards comaptiblity function', - sizeLimitInKB: 56, + sizeLimitInKB: 57, importsArray: [ importFromBase('createCloudinaryLegacyURL') ] From 8df1adb8c42699cf38020e2e460502358107aca1 Mon Sep 17 00:00:00 2001 From: Raya Straus Date: Sun, 15 Aug 2021 13:56:14 +0300 Subject: [PATCH 3/3] renamed to useDefaultValues --- __TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts | 2 +- src/backwards/configuration.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts b/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts index ca62cd5e..280d63f2 100644 --- a/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts +++ b/__TESTS_BUNDLE_SIZE__/bundleSizeTestCases.ts @@ -74,7 +74,7 @@ const bundleSizeTestCases:ITestCase[] = [ }, { name: 'Import all of the SDK', - sizeLimitInKB: 117, + sizeLimitInKB: 118, importsArray: [ importFromBase('CloudinaryBaseSDK') ] diff --git a/src/backwards/configuration.ts b/src/backwards/configuration.ts index a1b43905..95509460 100644 --- a/src/backwards/configuration.ts +++ b/src/backwards/configuration.ts @@ -15,7 +15,7 @@ import {isObject} from "./utils/isObject"; * @param {...Object} source - the source object(s) to assign defaults from * @return {Object} destination after it was modified */ -const defaults = (destination:{}, ...sources: object[])=>{ +const useDefaultValues = (destination:{}, ...sources: object[])=>{ return sources.reduce(function(dest, source) { let key, value; for (key in source) { @@ -42,7 +42,7 @@ class Configuration { private configuration: any; constructor(options: {}) { this.configuration = options == null ? {} : cloneDeep(options); - defaults(this.configuration, DEFAULT_CONFIGURATION_PARAMS); + useDefaultValues(this.configuration, DEFAULT_CONFIGURATION_PARAMS); } /**