cloudinary 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,9 +2,6 @@ require 'rspec'
2
2
  require 'spec_helper'
3
3
  require 'cloudinary'
4
4
 
5
- KEY = "00112233FF99"
6
- ALT_KEY = "CCBB2233FF00"
7
-
8
5
  describe 'auth_token' do
9
6
  before :all do
10
7
  @url_backup = ENV["CLOUDINARY_URL"]
@@ -4,8 +4,6 @@ require 'cloudinary'
4
4
  require 'action_view'
5
5
  require 'cloudinary/helper'
6
6
 
7
- KEY = "00112233FF99"
8
-
9
7
  helper_class = Class.new do
10
8
  include CloudinaryHelper
11
9
  end
@@ -7,9 +7,14 @@ TEST_IMAGE_URL = "http://cloudinary.com/images/old_logo.png"
7
7
  TEST_IMG = "spec/logo.png"
8
8
  TEST_IMG_W = 241
9
9
  TEST_IMG_H = 51
10
-
10
+ SUFFIX = ENV['TRAVIS_JOB_ID'] || rand(999999999).to_s
11
11
  TEST_TAG = 'cloudinary_gem_test'
12
- TIMESTAMP_TAG = "#{TEST_TAG}_#{rand(999999999)}_#{RUBY_VERSION}_#{ defined? Rails::version ? Rails::version : 'no_rails'}"
12
+ TIMESTAMP_TAG = "#{TEST_TAG}_#{SUFFIX}_#{RUBY_VERSION}_#{ defined? Rails::version ? Rails::version : 'no_rails'}"
13
+
14
+ # Auth token
15
+ KEY = "00112233FF99"
16
+ ALT_KEY = "CCBB2233FF00"
17
+
13
18
 
14
19
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
15
20
  RSpec.configure do |config|
@@ -99,11 +99,18 @@ describe Cloudinary::Uploader do
99
99
  result = Cloudinary::Uploader.upload(TEST_IMG, :eager =>"c_scale,w_2.0", :tags => [TEST_TAG, TIMESTAMP_TAG])
100
100
  expect(result["eager"].length).to be(1)
101
101
  expect(result).to have_deep_hash_values_of(["eager", 0, "transformation"] => "c_scale,w_2.0")
102
- result = Cloudinary::Uploader.upload(TEST_IMG, :eager =>["c_scale,w_2.0", { :crop =>"crop", :width =>"0.5", :format => "tiff"}], :tags => [TEST_TAG, TIMESTAMP_TAG])
103
- expect(result["eager"].length).to be(2)
102
+ result = Cloudinary::Uploader.upload(TEST_IMG, :eager =>[
103
+ "c_scale,w_2.0",
104
+ { :crop =>"crop", :width =>"0.5", :format => "tiff"},
105
+ [[{:crop =>"crop", :width =>"0.5"},{:angle =>90}]],
106
+ [[{:crop =>"crop", :width =>"0.5"},{:angle =>90}],"tiff"]
107
+ ], :tags => [TEST_TAG, TIMESTAMP_TAG])
108
+ expect(result["eager"].length).to be(4)
104
109
  expect(result).to have_deep_hash_values_of(
105
110
  ["eager", 0, "transformation"] => "c_scale,w_2.0",
106
- ["eager", 1, "transformation"] => "c_crop,w_0.5/tiff"
111
+ ["eager", 1, "transformation"] => "c_crop,w_0.5/tiff",
112
+ ["eager", 2, "transformation"] => "c_crop,w_0.5/a_90",
113
+ ["eager", 3, "transformation"] => "c_crop,w_0.5/a_90/tiff"
107
114
  )
108
115
  end
109
116
 
@@ -120,43 +127,95 @@ describe Cloudinary::Uploader do
120
127
 
121
128
  describe "tag" do
122
129
  describe "add_tag" do
123
- it "should correctly handle tags" do
130
+ it "should correctly add tags" do
124
131
  expected ={
125
132
  :url => /.*\/tags/,
126
133
  [:payload, :tag] => "new_tag",
127
- [:payload, :public_ids] => ["some_public_id"],
134
+ [:payload, :public_ids] => ["some_public_id1", "some_public_id2"],
128
135
  [:payload, :command] => "add"
129
136
  }
130
137
  expect(RestClient::Request).to receive(:execute).with(deep_hash_value(expected))
131
138
 
132
- Cloudinary::Uploader.add_tag( "new_tag", "some_public_id")
139
+ Cloudinary::Uploader.add_tag( "new_tag", ["some_public_id1", "some_public_id2"])
133
140
  end
134
- describe ":exclusive" do
135
- it "should support :exclusive" do
136
- expected ={
137
- :url => /.*\/tags/,
138
- [:payload, :tag] => "new_tag",
139
- [:payload, :public_ids] => ["some_public_id"],
140
- [:payload, :command] => "set_exclusive"
141
- }
142
- expect(RestClient::Request).to receive(:execute).with(deep_hash_value(expected))
143
-
144
- Cloudinary::Uploader.add_tag( "new_tag", "some_public_id", :exclusive => true)
145
- end
141
+ end
142
+
143
+ describe "remove_tag" do
144
+ it "should correctly remove tag" do
145
+ expected ={
146
+ :url => /.*\/tags/,
147
+ [:payload, :tag] => "tag",
148
+ [:payload, :public_ids] => ["some_public_id1", "some_public_id2"],
149
+ [:payload, :command] => "remove"
150
+ }
151
+ expect(RestClient::Request).to receive(:execute).with(deep_hash_value(expected))
152
+
153
+ Cloudinary::Uploader.remove_tag("tag", ["some_public_id1", "some_public_id2"])
146
154
  end
155
+ end
156
+
157
+ describe "replace_tag" do
158
+ it "should correctly replace tag" do
159
+ expected ={
160
+ :url => /.*\/tags/,
161
+ [:payload, :tag] => "tag",
162
+ [:payload, :public_ids] => ["some_public_id1", "some_public_id2"],
163
+ [:payload, :command] => "replace"
164
+ }
165
+ expect(RestClient::Request).to receive(:execute).with(deep_hash_value(expected))
147
166
 
167
+ Cloudinary::Uploader.replace_tag("tag", ["some_public_id1", "some_public_id2"])
168
+ end
169
+ end
148
170
 
171
+ describe "remove_all_tags" do
172
+ it "should correctly remove all tags" do
173
+ expected ={
174
+ :url => /.*\/tags/,
175
+ [:payload, :public_ids] => ["some_public_id1", "some_public_id2"],
176
+ [:payload, :command] => "remove_all"
177
+ }
178
+ expect(RestClient::Request).to receive(:execute).with(deep_hash_value(expected))
149
179
 
180
+ Cloudinary::Uploader.remove_all_tags(["some_public_id1", "some_public_id2"])
181
+ end
150
182
  end
151
183
 
152
- # Cloudinary::Uploader.add_tag("#{new_tag}_2", result["public_id"])
153
- # expect(Cloudinary::Api.resource(result["public_id"])["tags"]).to match_array(["#{new_tag}_1", "#{new_tag}_2", TEST_TAG, TIMESTAMP_TAG])
154
- # Cloudinary::Uploader.remove_tag("#{new_tag}_1", result["public_id"])
155
- # expect(Cloudinary::Api.resource(result["public_id"])["tags"]).to match_array(["#{new_tag}_2", TEST_TAG, TIMESTAMP_TAG])
156
- # Cloudinary::Uploader.replace_tag("#{new_tag}_3", result["public_id"])
157
- # expect(Cloudinary::Api.resource(result["public_id"])["tags"]).to match_array(["#{new_tag}_3"])
184
+ end
185
+
186
+
187
+ describe "context", :focus => true do
188
+ describe "add_context" do
189
+ it "should correctly add context", :focus => true do
190
+ expected ={
191
+ :url => /.*\/context/,
192
+ [:payload, :context] => "key1=value1|key2=value2",
193
+ [:payload, :public_ids] => ["some_public_id1", "some_public_id2"],
194
+ [:payload, :command] => "add"
195
+ }
196
+ expect(RestClient::Request).to receive(:execute).with(deep_hash_value(expected))
197
+
198
+ Cloudinary::Uploader.add_context( {:key1 => :value1, :key2 => :value2}, ["some_public_id1", "some_public_id2"])
199
+ end
158
200
  end
159
201
 
202
+ describe "remove_all_context" do
203
+ it "should correctly remove all context", :focus => true do
204
+ expected ={
205
+ :url => /.*\/context/,
206
+ [:payload, :public_ids] => ["some_public_id1", "some_public_id2"],
207
+ [:payload, :command] => "remove_all",
208
+ [:payload, :type] => "private"
209
+
210
+ }
211
+ expect(RestClient::Request).to receive(:execute).with(deep_hash_value(expected))
212
+
213
+ Cloudinary::Uploader.remove_all_context(["some_public_id1", "some_public_id2"], :type => "private")
214
+ end
215
+ end
216
+
217
+ end
218
+
160
219
 
161
220
 
162
221
  it "should correctly handle unique_filename" do
@@ -193,7 +252,7 @@ describe Cloudinary::Uploader do
193
252
  end
194
253
 
195
254
  it "should allow sending context" do
196
- context = {"caption" => "some caption", "alt" => "alternative"}
255
+ context = {"key1"=>'value1', "key2" => 'valu\e2', "key3" => 'val=u|e3', "key4" => 'val\=ue'}
197
256
  result = Cloudinary::Uploader.upload(TEST_IMG, { :context => context, :tags => [TEST_TAG, TIMESTAMP_TAG]})
198
257
  info = Cloudinary::Api.resource(result["public_id"], {:context => true})
199
258
  expect(info["context"]).to eq({"custom" => context})
@@ -236,6 +295,14 @@ describe Cloudinary::Uploader do
236
295
  expect(result["height"]).to eq(1400)
237
296
  expect(result["format"]).to eq("bmp")
238
297
  end
298
+
299
+ it "should allow fallback of upload large with remote url to regular upload", :focus => true do
300
+ file = "http://cloudinary.com/images/old_logo.png"
301
+ result = Cloudinary::Uploader.upload_large(file, :chunk_size => 5243000, :tags => [TEST_TAG, TIMESTAMP_TAG])
302
+ expect(result).to_not be_nil
303
+ expect(result["width"]).to eq(TEST_IMG_W)
304
+ expect(result["height"]).to eq(TEST_IMG_H)
305
+ end
239
306
 
240
307
  context "unsigned" do
241
308
  after do
@@ -271,13 +338,22 @@ describe Cloudinary::Uploader do
271
338
  end
272
339
 
273
340
  context ":responsive_breakpoints" do
274
- context ":create_derived" do
275
- result = Cloudinary::Uploader.upload(TEST_IMG, :responsive_breakpoints => { :create_derived => false }, :tags => [TEST_TAG, TIMESTAMP_TAG])
276
- it 'should return a responsive_breakpoints in the response' do
277
- expect(result).to include('responsive_breakpoints')
341
+ context ":create_derived with transformation and format conversion" do
342
+ expected ={
343
+ :url => /.*\/upload$/,
344
+ [:payload, :responsive_breakpoints] => %r("transformation":"e_sepia/jpg"),
345
+ [:payload, :responsive_breakpoints] => %r("transformation":"gif"),
346
+ [:payload, :responsive_breakpoints] => %r("create_derived":true)
347
+ }
348
+ it 'should return a proper responsive_breakpoints hash in the response' do
349
+ expect(RestClient::Request).to receive(:execute).with(deep_hash_value(expected))
350
+ Cloudinary::Uploader.upload(TEST_IMG, responsive_breakpoints:[{transformation:{effect: "sepia"}, format:"jpg", bytes_step:20000, create_derived: true, :min_width => 200, :max_width => 1000, :max_images => 20},{format:"gif", create_derived:true, bytes_step:20000, :min_width => 200, :max_width => 1000, :max_images => 20}], :tags => [TEST_TAG, TIMESTAMP_TAG])
278
351
  end
279
352
  end
280
353
  end
354
+
355
+
356
+
281
357
  describe 'explicit' do
282
358
 
283
359
  context ":invalidate" do
@@ -144,7 +144,7 @@ describe Cloudinary::Utils do
144
144
  .and empty_options
145
145
  end
146
146
 
147
- it "should support url_suffix for private uploads" do
147
+ it "should support url_suffix for private images" do
148
148
  expect(["test", { :url_suffix => "hello", :private_cdn => true, :resource_type => :image, :type => :private }])
149
149
  .to produce_url("http://#{cloud_name}-res.cloudinary.com/private_images/test/hello")
150
150
  .and empty_options
@@ -153,6 +153,13 @@ describe Cloudinary::Utils do
153
153
  .and empty_options
154
154
  end
155
155
 
156
+ it "should support url_suffix for authenticated images" do
157
+ expect(["test", { :url_suffix => "hello", :format => "jpg", :resource_type => :image, :type => :authenticated }])
158
+ .to produce_url("http://res.cloudinary.com/#{cloud_name}/authenticated_images/test/hello.jpg")
159
+ .and empty_options
160
+ end
161
+
162
+
156
163
  describe 'root_path support' do
157
164
 
158
165
  it "should allow use_root_path in shared distribution" do
@@ -386,8 +393,8 @@ describe Cloudinary::Utils do
386
393
  end
387
394
 
388
395
  it "should support effect with hash param" do
389
- expect(["test", { :effect => { "sepia" => 10 } }])
390
- .to produce_url("#{upload_path}/e_sepia:10/test")
396
+ expect(["test", { :effect => { "sepia" => -10 } }])
397
+ .to produce_url("#{upload_path}/e_sepia:-10/test")
391
398
  .and empty_options
392
399
  end
393
400
 
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * Cloudinary's JavaScript library - Version 2.1.9
3
+ * Cloudinary's JavaScript library - Version 2.3.0
4
4
  * Copyright Cloudinary
5
5
  * see https://github.com/cloudinary/cloudinary_js
6
6
  *
@@ -30,7 +30,7 @@ var slice = [].slice,
30
30
  /*
31
31
  * Includes common utility methods and shims
32
32
  */
33
- var ArrayParam, BaseUtil, ClientHintsMetaTag, Cloudinary, CloudinaryJQuery, Condition, Configuration, HtmlTag, ImageTag, Layer, LayerParam, Param, RangeParam, RawParam, SubtitlesLayer, TextLayer, Transformation, TransformationBase, TransformationParam, Util, VideoTag, addClass, allStrings, camelCase, cloneDeep, cloudinary, compact, contains, convertKeys, crc32, defaults, difference, functions, getAttribute, getData, hasClass, identity, isEmpty, isNumberLike, isString, m, merge, parameters, reWords, removeAttribute, setAttribute, setAttributes, setData, smartEscape, snakeCase, utf8_encode, webp, width, withCamelCaseKeys, withSnakeCaseKeys, without;
33
+ var ArrayParam, BaseUtil, ClientHintsMetaTag, Cloudinary, CloudinaryJQuery, Condition, Configuration, Expression, ExpressionParam, HtmlTag, ImageTag, Layer, LayerParam, Param, RangeParam, RawParam, SubtitlesLayer, TextLayer, Transformation, TransformationBase, TransformationParam, Util, VideoTag, addClass, allStrings, camelCase, cloneDeep, cloudinary, compact, contains, convertKeys, crc32, defaults, difference, functions, getAttribute, getData, hasClass, identity, isEmpty, isNumberLike, isString, m, merge, parameters, reWords, removeAttribute, setAttribute, setAttributes, setData, smartEscape, snakeCase, utf8_encode, webp, width, withCamelCaseKeys, withSnakeCaseKeys, without;
34
34
  allStrings = function(list) {
35
35
  var item, j, len;
36
36
  for (j = 0, len = list.length; j < len; j++) {
@@ -683,7 +683,7 @@ var slice = [].slice,
683
683
  */
684
684
 
685
685
  TextLayer.prototype.toString = function() {
686
- var components, hasPublicId, hasStyle, publicId, style, text;
686
+ var components, hasPublicId, hasStyle, publicId, re, res, start, style, text, textSource;
687
687
  style = this.textStyleIdentifier();
688
688
  if (this.options.publicId != null) {
689
689
  publicId = this.getFullPublicId();
@@ -694,7 +694,16 @@ var slice = [].slice,
694
694
  if (hasPublicId && hasStyle || !hasPublicId && !hasStyle) {
695
695
  throw "Must supply either style parameters or a public_id when providing text parameter in a text overlay/underlay, but not both!";
696
696
  }
697
- text = Util.smartEscape(Util.smartEscape(this.options.text, /[,\/]/g));
697
+ re = /\$\([a-zA-Z]\w*\)/g;
698
+ start = 0;
699
+ textSource = Util.smartEscape(this.options.text, /[,\/]/g);
700
+ text = "";
701
+ while (res = re.exec(textSource)) {
702
+ text += Util.smartEscape(textSource.slice(start, res.index));
703
+ text += res[0];
704
+ start = res.index + res[0].length;
705
+ }
706
+ text += Util.smartEscape(textSource.slice(start));
698
707
  }
699
708
  components = [this.options.resourceType, style, publicId, text];
700
709
  return Util.compact(components).join(":");
@@ -916,18 +925,19 @@ var slice = [].slice,
916
925
  }
917
926
 
918
927
  ArrayParam.prototype.serialize = function() {
919
- var array, flat, t;
928
+ var arrayValue, flat, t;
920
929
  if (this.shortName != null) {
921
- array = this.value();
922
- if (cloudinary.Util.isEmpty(array)) {
930
+ arrayValue = this.value();
931
+ if (cloudinary.Util.isEmpty(arrayValue)) {
923
932
  return '';
933
+ } else if (cloudinary.Util.isString(arrayValue)) {
934
+ return this.shortName + "_" + arrayValue;
924
935
  } else {
925
936
  flat = (function() {
926
- var j, len, ref, results;
927
- ref = this.value();
937
+ var j, len, results;
928
938
  results = [];
929
- for (j = 0, len = ref.length; j < len; j++) {
930
- t = ref[j];
939
+ for (j = 0, len = arrayValue.length; j < len; j++) {
940
+ t = arrayValue[j];
931
941
  if (cloudinary.Util.isFunction(t.serialize)) {
932
942
  results.push(t.serialize());
933
943
  } else {
@@ -935,7 +945,7 @@ var slice = [].slice,
935
945
  }
936
946
  }
937
947
  return results;
938
- }).call(this);
948
+ })();
939
949
  return this.shortName + "_" + (flat.join(this.sep));
940
950
  }
941
951
  } else {
@@ -943,6 +953,21 @@ var slice = [].slice,
943
953
  }
944
954
  };
945
955
 
956
+ ArrayParam.prototype.value = function() {
957
+ var j, len, ref, results, v;
958
+ if (cloudinary.Util.isArray(this.origValue)) {
959
+ ref = this.origValue;
960
+ results = [];
961
+ for (j = 0, len = ref.length; j < len; j++) {
962
+ v = ref[j];
963
+ results.push(this.process(v));
964
+ }
965
+ return results;
966
+ } else {
967
+ return this.process(this.origValue);
968
+ }
969
+ };
970
+
946
971
  ArrayParam.prototype.set = function(origValue) {
947
972
  if ((origValue == null) || cloudinary.Util.isArray(origValue)) {
948
973
  return ArrayParam.__super__.set.call(this, origValue);
@@ -1114,6 +1139,20 @@ var slice = [].slice,
1114
1139
 
1115
1140
  return LayerParam;
1116
1141
 
1142
+ })(Param);
1143
+ ExpressionParam = (function(superClass) {
1144
+ extend(ExpressionParam, superClass);
1145
+
1146
+ function ExpressionParam() {
1147
+ return ExpressionParam.__super__.constructor.apply(this, arguments);
1148
+ }
1149
+
1150
+ ExpressionParam.prototype.serialize = function() {
1151
+ return Expression.normalize(ExpressionParam.__super__.serialize.call(this));
1152
+ };
1153
+
1154
+ return ExpressionParam;
1155
+
1117
1156
  })(Param);
1118
1157
  parameters = {};
1119
1158
  parameters.Param = Param;
@@ -1122,12 +1161,15 @@ var slice = [].slice,
1122
1161
  parameters.RawParam = RawParam;
1123
1162
  parameters.TransformationParam = TransformationParam;
1124
1163
  parameters.LayerParam = LayerParam;
1125
- Condition = (function() {
1164
+ parameters.ExpressionParam = ExpressionParam;
1165
+ Expression = (function() {
1126
1166
 
1127
1167
  /**
1128
1168
  * @internal
1129
1169
  */
1130
- Condition.OPERATORS = {
1170
+ var faceCount;
1171
+
1172
+ Expression.OPERATORS = {
1131
1173
  "=": 'eq',
1132
1174
  "!=": 'ne',
1133
1175
  "<": 'lt',
@@ -1135,159 +1177,412 @@ var slice = [].slice,
1135
1177
  "<=": 'lte',
1136
1178
  ">=": 'gte',
1137
1179
  "&&": 'and',
1138
- "||": 'or'
1180
+ "||": 'or',
1181
+ "*": "mul",
1182
+ "/": "div",
1183
+ "+": "add",
1184
+ "-": "sub"
1139
1185
  };
1140
1186
 
1141
- Condition.PARAMETERS = {
1142
- "width": "w",
1143
- "height": "h",
1187
+
1188
+ /**
1189
+ * @internal
1190
+ */
1191
+
1192
+ Expression.PREDEFINED_VARS = {
1144
1193
  "aspect_ratio": "ar",
1145
1194
  "aspectRatio": "ar",
1195
+ "current_page": "cp",
1196
+ "currentPage": "cp",
1197
+ "face_count": "fc",
1198
+ "faceCount": "fc",
1199
+ "height": "h",
1200
+ "initial_aspect_ratio": "iar",
1201
+ "initial_height": "ih",
1202
+ "initial_width": "iw",
1203
+ "initialAspectRatio": "iar",
1204
+ "initialHeight": "ih",
1205
+ "initialWidth": "iw",
1146
1206
  "page_count": "pc",
1207
+ "page_x": "px",
1208
+ "page_y": "py",
1147
1209
  "pageCount": "pc",
1148
- "face_count": "fc",
1149
- "faceCount": "fc"
1210
+ "pageX": "px",
1211
+ "pageY": "py",
1212
+ "tags": "tags",
1213
+ "width": "w"
1150
1214
  };
1151
1215
 
1152
- Condition.BOUNDRY = "[ _]+";
1216
+
1217
+ /**
1218
+ * @internal
1219
+ */
1220
+
1221
+ Expression.BOUNDRY = "[ _]+";
1153
1222
 
1154
1223
 
1155
1224
  /**
1156
- * Represents a transformation condition
1157
- * @param {string} conditionStr - a condition in string format
1158
- * @class Condition
1159
- * @example
1160
- * // normally this class is not instantiated directly
1161
- * var tr = cloudinary.Transformation.new()
1162
- * .if().width( ">", 1000).and().aspectRatio("<", "3:4").then()
1163
- * .width(1000)
1164
- * .crop("scale")
1165
- * .else()
1166
- * .width(500)
1167
- * .crop("scale")
1168
- *
1169
- * var tr = cloudinary.Transformation.new()
1170
- * .if("w > 1000 and aspectRatio < 3:4")
1171
- * .width(1000)
1172
- * .crop("scale")
1173
- * .else()
1174
- * .width(500)
1175
- * .crop("scale")
1225
+ * Represents a transformation expression
1226
+ * @param {string} expressionStr - a expression in string format
1227
+ * @class Expression
1176
1228
  *
1177
1229
  */
1178
1230
 
1179
- function Condition(conditionStr) {
1180
- this.predicate_list = [];
1181
- if (conditionStr != null) {
1182
- this.predicate_list.push(this.normalize(conditionStr));
1231
+ function Expression(expressionStr) {
1232
+
1233
+ /**
1234
+ * @protected
1235
+ * @inner Expression-expressions
1236
+ */
1237
+ this.expressions = [];
1238
+ if (expressionStr != null) {
1239
+ this.expressions.push(Expression.normalize(expressionStr));
1183
1240
  }
1184
1241
  }
1185
1242
 
1186
1243
 
1187
1244
  /**
1188
1245
  * Convenience constructor method
1189
- * @function Condition.new
1246
+ * @function Expression.new
1190
1247
  */
1191
1248
 
1192
- Condition["new"] = function(conditionStr) {
1193
- return new this(conditionStr);
1249
+ Expression["new"] = function(expressionStr) {
1250
+ return new this(expressionStr);
1194
1251
  };
1195
1252
 
1196
1253
 
1197
1254
  /**
1198
- * Normalize a string condition
1255
+ * Normalize a string expression
1199
1256
  * @function Cloudinary#normalize
1200
- * @param {string} value a condition, e.g. "w gt 100", "width_gt_100", "width > 100"
1201
- * @return {string} the normalized form of the value condition, e.g. "w_gt_100"
1257
+ * @param {string} expression a expression, e.g. "w gt 100", "width_gt_100", "width > 100"
1258
+ * @return {string} the normalized form of the value expression, e.g. "w_gt_100"
1202
1259
  */
1203
1260
 
1204
- Condition.prototype.normalize = function(value) {
1205
- var replaceRE;
1206
- replaceRE = new RegExp("(" + Object.keys(Condition.PARAMETERS).join("|") + "|[=<>&|!]+)", "g");
1207
- value = value.replace(replaceRE, function(match) {
1208
- return Condition.OPERATORS[match] || Condition.PARAMETERS[match];
1261
+ Expression.normalize = function(expression) {
1262
+ var operators, pattern, replaceRE;
1263
+ if (expression == null) {
1264
+ return expression;
1265
+ }
1266
+ expression = String(expression);
1267
+ operators = "\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*";
1268
+ pattern = "((" + operators + ")(?=[ _])|" + Object.keys(Expression.PREDEFINED_VARS).join("|") + ")";
1269
+ replaceRE = new RegExp(pattern, "g");
1270
+ expression = expression.replace(replaceRE, function(match) {
1271
+ return Expression.OPERATORS[match] || Expression.PREDEFINED_VARS[match];
1209
1272
  });
1210
- return value.replace(/[ _]+/g, '_');
1273
+ return expression.replace(/[ _]+/g, '_');
1274
+ };
1275
+
1276
+
1277
+ /**
1278
+ * Serialize the expression
1279
+ * @return {string} the expression as a string
1280
+ */
1281
+
1282
+ Expression.prototype.serialize = function() {
1283
+ return Expression.normalize(this.expressions.join("_"));
1284
+ };
1285
+
1286
+ Expression.prototype.toString = function() {
1287
+ return this.serialize();
1211
1288
  };
1212
1289
 
1213
1290
 
1214
1291
  /**
1215
- * Get the parent transformation of this condition
1292
+ * Get the parent transformation of this expression
1216
1293
  * @return Transformation
1217
1294
  */
1218
1295
 
1219
- Condition.prototype.getParent = function() {
1296
+ Expression.prototype.getParent = function() {
1220
1297
  return this.parent;
1221
1298
  };
1222
1299
 
1223
1300
 
1224
1301
  /**
1225
- * Set the parent transformation of this condition
1302
+ * Set the parent transformation of this expression
1226
1303
  * @param {Transformation} the parent transformation
1227
- * @return {Condition} this condition
1304
+ * @return {Expression} this expression
1228
1305
  */
1229
1306
 
1230
- Condition.prototype.setParent = function(parent) {
1307
+ Expression.prototype.setParent = function(parent) {
1231
1308
  this.parent = parent;
1232
1309
  return this;
1233
1310
  };
1234
1311
 
1235
1312
 
1236
1313
  /**
1237
- * Serialize the condition
1238
- * @return {string} the condition as a string
1314
+ * Add a expression
1315
+ * @function Expression#predicate
1316
+ * @internal
1239
1317
  */
1240
1318
 
1241
- Condition.prototype.toString = function() {
1242
- return this.predicate_list.join("_");
1319
+ Expression.prototype.predicate = function(name, operator, value) {
1320
+ if (Expression.OPERATORS[operator] != null) {
1321
+ operator = Expression.OPERATORS[operator];
1322
+ }
1323
+ this.expressions.push(name + "_" + operator + "_" + value);
1324
+ return this;
1243
1325
  };
1244
1326
 
1245
1327
 
1246
1328
  /**
1247
- * Add a condition
1248
- * @function Condition#predicate
1249
- * @internal
1329
+ * @function Expression#and
1250
1330
  */
1251
1331
 
1252
- Condition.prototype.predicate = function(name, operator, value) {
1253
- if (Condition.OPERATORS[operator] != null) {
1254
- operator = Condition.OPERATORS[operator];
1255
- }
1256
- this.predicate_list.push(name + "_" + operator + "_" + value);
1332
+ Expression.prototype.and = function() {
1333
+ this.expressions.push("and");
1257
1334
  return this;
1258
1335
  };
1259
1336
 
1260
1337
 
1261
1338
  /**
1262
- * @function Condition#and
1339
+ * @function Expression#or
1263
1340
  */
1264
1341
 
1265
- Condition.prototype.and = function() {
1266
- this.predicate_list.push("and");
1342
+ Expression.prototype.or = function() {
1343
+ this.expressions.push("or");
1267
1344
  return this;
1268
1345
  };
1269
1346
 
1270
1347
 
1271
1348
  /**
1272
- * @function Condition#or
1349
+ * Conclude expression
1350
+ * @function Expression#then
1351
+ * @return {Transformation} the transformation this expression is defined for
1352
+ */
1353
+
1354
+ Expression.prototype.then = function() {
1355
+ return this.getParent()["if"](this.toString());
1356
+ };
1357
+
1358
+
1359
+ /**
1360
+ * @function Expression#height
1361
+ * @param {string} operator the comparison operator (e.g. "<", "lt")
1362
+ * @param {string|number} value the right hand side value
1363
+ * @return {Expression} this expression
1364
+ */
1365
+
1366
+ Expression.prototype.height = function(operator, value) {
1367
+ return this.predicate("h", operator, value);
1368
+ };
1369
+
1370
+
1371
+ /**
1372
+ * @function Expression#width
1373
+ * @param {string} operator the comparison operator (e.g. "<", "lt")
1374
+ * @param {string|number} value the right hand side value
1375
+ * @return {Expression} this expression
1376
+ */
1377
+
1378
+ Expression.prototype.width = function(operator, value) {
1379
+ return this.predicate("w", operator, value);
1380
+ };
1381
+
1382
+
1383
+ /**
1384
+ * @function Expression#aspectRatio
1385
+ * @param {string} operator the comparison operator (e.g. "<", "lt")
1386
+ * @param {string|number} value the right hand side value
1387
+ * @return {Expression} this expression
1388
+ */
1389
+
1390
+ Expression.prototype.aspectRatio = function(operator, value) {
1391
+ return this.predicate("ar", operator, value);
1392
+ };
1393
+
1394
+
1395
+ /**
1396
+ * @function Expression#pages
1397
+ * @param {string} operator the comparison operator (e.g. "<", "lt")
1398
+ * @param {string|number} value the right hand side value
1399
+ * @return {Expression} this expression
1400
+ */
1401
+
1402
+ Expression.prototype.pageCount = function(operator, value) {
1403
+ return this.predicate("pc", operator, value);
1404
+ };
1405
+
1406
+
1407
+ /**
1408
+ * @function Expression#faces
1409
+ * @param {string} operator the comparison operator (e.g. "<", "lt")
1410
+ * @param {string|number} value the right hand side value
1411
+ * @return {Expression} this expression
1273
1412
  */
1274
1413
 
1275
- Condition.prototype.or = function() {
1276
- this.predicate_list.push("or");
1414
+ Expression.prototype.faceCount = function(operator, value) {
1415
+ return this.predicate("fc", operator, value);
1416
+ };
1417
+
1418
+ Expression.prototype.value = function(value) {
1419
+ this.expressions.push(value);
1277
1420
  return this;
1278
1421
  };
1279
1422
 
1280
1423
 
1281
1424
  /**
1282
- * Conclude condition
1283
- * @function Condition#then
1284
- * @return {Transformation} the transformation this condition is defined for
1285
1425
  */
1286
1426
 
1287
- Condition.prototype.then = function() {
1288
- return this.getParent()["if"](this.toString());
1427
+ Expression.variable = function(name, value) {
1428
+ return new this(name).value(value);
1429
+ };
1430
+
1431
+
1432
+ /**
1433
+ * @returns a new expression with the predefined variable "width"
1434
+ * @function Expression.width
1435
+ */
1436
+
1437
+ Expression.width = function() {
1438
+ return new this("width");
1439
+ };
1440
+
1441
+
1442
+ /**
1443
+ * @returns a new expression with the predefined variable "height"
1444
+ * @function Expression.height
1445
+ */
1446
+
1447
+ Expression.height = function() {
1448
+ return new this("height");
1449
+ };
1450
+
1451
+
1452
+ /**
1453
+ * @returns a new expression with the predefined variable "initialWidth"
1454
+ * @function Expression.initialWidth
1455
+ */
1456
+
1457
+ Expression.initialWidth = function() {
1458
+ return new this("initialWidth");
1459
+ };
1460
+
1461
+
1462
+ /**
1463
+ * @returns a new expression with the predefined variable "initialHeight"
1464
+ * @function Expression.initialHeight
1465
+ */
1466
+
1467
+ Expression.initialHeight = function() {
1468
+ return new this("initialHeight");
1469
+ };
1470
+
1471
+
1472
+ /**
1473
+ * @returns a new expression with the predefined variable "aspectRatio"
1474
+ * @function Expression.aspectRatio
1475
+ */
1476
+
1477
+ Expression.aspectRatio = function() {
1478
+ return new this("aspectRatio");
1479
+ };
1480
+
1481
+
1482
+ /**
1483
+ * @returns a new expression with the predefined variable "initialAspectRatio"
1484
+ * @function Expression.initialAspectRatio
1485
+ */
1486
+
1487
+ Expression.initialAspectRatio = function() {
1488
+ return new this("initialAspectRatio");
1489
+ };
1490
+
1491
+
1492
+ /**
1493
+ * @returns a new expression with the predefined variable "pageCount"
1494
+ * @function Expression.pageCount
1495
+ */
1496
+
1497
+ Expression.pageCount = function() {
1498
+ return new this("pageCount");
1499
+ };
1500
+
1501
+
1502
+ /**
1503
+ * @returns a new expression with the predefined variable "faceCount"
1504
+ * @function Expression.faceCount
1505
+ */
1506
+
1507
+ faceCount = function() {
1508
+ return new this("faceCount");
1509
+ };
1510
+
1511
+
1512
+ /**
1513
+ * @returns a new expression with the predefined variable "currentPage"
1514
+ * @function Expression.currentPage
1515
+ */
1516
+
1517
+ Expression.currentPage = function() {
1518
+ return new this("currentPage");
1519
+ };
1520
+
1521
+
1522
+ /**
1523
+ * @returns a new expression with the predefined variable "tags"
1524
+ * @function Expression.tags
1525
+ */
1526
+
1527
+ Expression.tags = function() {
1528
+ return new this("tags");
1529
+ };
1530
+
1531
+
1532
+ /**
1533
+ * @returns a new expression with the predefined variable "pageX"
1534
+ * @function Expression.pageX
1535
+ */
1536
+
1537
+ Expression.pageX = function() {
1538
+ return new this("pageX");
1539
+ };
1540
+
1541
+
1542
+ /**
1543
+ * @returns a new expression with the predefined variable "pageY"
1544
+ * @function Expression.pageY
1545
+ */
1546
+
1547
+ Expression.pageY = function() {
1548
+ return new this("pageY");
1289
1549
  };
1290
1550
 
1551
+ return Expression;
1552
+
1553
+ })();
1554
+ Condition = (function(superClass) {
1555
+ extend(Condition, superClass);
1556
+
1557
+
1558
+ /**
1559
+ * Represents a transformation condition
1560
+ * @param {string} conditionStr - a condition in string format
1561
+ * @class Condition
1562
+ * @example
1563
+ * // normally this class is not instantiated directly
1564
+ * var tr = cloudinary.Transformation.new()
1565
+ * .if().width( ">", 1000).and().aspectRatio("<", "3:4").then()
1566
+ * .width(1000)
1567
+ * .crop("scale")
1568
+ * .else()
1569
+ * .width(500)
1570
+ * .crop("scale")
1571
+ *
1572
+ * var tr = cloudinary.Transformation.new()
1573
+ * .if("w > 1000 and aspectRatio < 3:4")
1574
+ * .width(1000)
1575
+ * .crop("scale")
1576
+ * .else()
1577
+ * .width(500)
1578
+ * .crop("scale")
1579
+ *
1580
+ */
1581
+
1582
+ function Condition(conditionStr) {
1583
+ Condition.__super__.constructor.call(this, conditionStr);
1584
+ }
1585
+
1291
1586
 
1292
1587
  /**
1293
1588
  * @function Condition#height
@@ -1350,7 +1645,7 @@ var slice = [].slice,
1350
1645
 
1351
1646
  return Condition;
1352
1647
 
1353
- })();
1648
+ })(Expression);
1354
1649
 
1355
1650
  /**
1356
1651
  * Cloudinary configuration class
@@ -1370,7 +1665,7 @@ var slice = [].slice,
1370
1665
  secure: (typeof window !== "undefined" && window !== null ? (ref = window.location) != null ? ref.protocol : void 0 : void 0) === 'https:'
1371
1666
  };
1372
1667
 
1373
- Configuration.CONFIG_PARAMS = ["api_key", "api_secret", "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", "url_suffix", "use_root_path", "version"];
1668
+ Configuration.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"];
1374
1669
 
1375
1670
 
1376
1671
  /**
@@ -1559,7 +1854,9 @@ var slice = [].slice,
1559
1854
  * @internal
1560
1855
  */
1561
1856
  TransformationBase = (function() {
1562
- var lastArgCallback;
1857
+ var VAR_NAME_RE, lastArgCallback, processVar;
1858
+
1859
+ VAR_NAME_RE = /^\$[a-zA-Z0-9]+$/;
1563
1860
 
1564
1861
  TransformationBase.prototype.trans_separator = '/';
1565
1862
 
@@ -1600,7 +1897,7 @@ var slice = [].slice,
1600
1897
  * @return {Object} Returns a plain object representing this transformation
1601
1898
  */
1602
1899
  this.toOptions || (this.toOptions = function(withChain) {
1603
- var key, list, opt, ref, tr, value;
1900
+ var key, list, opt, ref, ref1, tr, value;
1604
1901
  if (withChain == null) {
1605
1902
  withChain = true;
1606
1903
  }
@@ -1628,9 +1925,15 @@ var slice = [].slice,
1628
1925
  return results;
1629
1926
  }).call(this);
1630
1927
  list.push(opt);
1631
- opt = {
1632
- transformation: list
1633
- };
1928
+ opt = {};
1929
+ ref1 = this.otherOptions;
1930
+ for (key in ref1) {
1931
+ value = ref1[key];
1932
+ if (value !== void 0) {
1933
+ opt[key] = value;
1934
+ }
1935
+ }
1936
+ opt.transformation = list;
1634
1937
  }
1635
1938
  return opt;
1636
1939
  });
@@ -1781,7 +2084,9 @@ var slice = [].slice,
1781
2084
  var results;
1782
2085
  results = [];
1783
2086
  for (key in trans) {
1784
- results.push(Util.snakeCase(key));
2087
+ if (key != null) {
2088
+ results.push(key.match(VAR_NAME_RE) ? key : Util.snakeCase(key));
2089
+ }
1785
2090
  }
1786
2091
  return results;
1787
2092
  })()).sort();
@@ -1874,9 +2179,19 @@ var slice = [].slice,
1874
2179
  return new value.constructor(value.toOptions());
1875
2180
  }
1876
2181
  });
2182
+ if (options["if"]) {
2183
+ this.set("if", options["if"]);
2184
+ delete options["if"];
2185
+ }
1877
2186
  for (key in options) {
1878
2187
  opt = options[key];
1879
- this.set(key, opt);
2188
+ if (key.match(VAR_NAME_RE)) {
2189
+ if (key !== '$attr') {
2190
+ this.set('variable', key, opt);
2191
+ }
2192
+ } else {
2193
+ this.set(key, opt);
2194
+ }
1880
2195
  }
1881
2196
  }
1882
2197
  return this;
@@ -1903,13 +2218,14 @@ var slice = [].slice,
1903
2218
  * @returns {Transformation} Returns this instance for chaining
1904
2219
  */
1905
2220
 
1906
- TransformationBase.prototype.set = function(key, value) {
1907
- var camelKey;
2221
+ TransformationBase.prototype.set = function() {
2222
+ var camelKey, key, values;
2223
+ key = arguments[0], values = 2 <= arguments.length ? slice.call(arguments, 1) : [];
1908
2224
  camelKey = Util.camelCase(key);
1909
2225
  if (Util.contains(Transformation.methods, camelKey)) {
1910
- this[camelKey](value);
2226
+ this[camelKey].apply(this, values);
1911
2227
  } else {
1912
- this.otherOptions[key] = value;
2228
+ this.otherOptions[key] = values[0];
1913
2229
  }
1914
2230
  return this;
1915
2231
  };
@@ -1926,7 +2242,7 @@ var slice = [].slice,
1926
2242
  */
1927
2243
 
1928
2244
  TransformationBase.prototype.serialize = function() {
1929
- var ifParam, paramList, ref, ref1, resultArray, t, tr, transformationList, transformationString, transformations, value;
2245
+ var ifParam, j, len, paramList, ref, ref1, ref2, ref3, ref4, resultArray, t, tr, transformationList, transformationString, transformations, value, variables, vars;
1930
2246
  resultArray = (function() {
1931
2247
  var j, len, ref, results;
1932
2248
  ref = this.chained;
@@ -1940,16 +2256,18 @@ var slice = [].slice,
1940
2256
  paramList = this.keys();
1941
2257
  transformations = (ref = this.get("transformation")) != null ? ref.serialize() : void 0;
1942
2258
  ifParam = (ref1 = this.get("if")) != null ? ref1.serialize() : void 0;
1943
- paramList = Util.difference(paramList, ["transformation", "if"]);
1944
- transformationList = (function() {
1945
- var j, len, ref2, results;
1946
- results = [];
1947
- for (j = 0, len = paramList.length; j < len; j++) {
1948
- t = paramList[j];
1949
- results.push((ref2 = this.get(t)) != null ? ref2.serialize() : void 0);
2259
+ variables = processVar((ref2 = this.get("variables")) != null ? ref2.value() : void 0);
2260
+ paramList = Util.difference(paramList, ["transformation", "if", "variables"]);
2261
+ vars = [];
2262
+ transformationList = [];
2263
+ for (j = 0, len = paramList.length; j < len; j++) {
2264
+ t = paramList[j];
2265
+ if (t.match(VAR_NAME_RE)) {
2266
+ vars.push(t + "_" + Expression.normalize((ref3 = this.get(t)) != null ? ref3.value() : void 0));
2267
+ } else {
2268
+ transformationList.push((ref4 = this.get(t)) != null ? ref4.serialize() : void 0);
1950
2269
  }
1951
- return results;
1952
- }).call(this);
2270
+ }
1953
2271
  switch (false) {
1954
2272
  case !Util.isString(transformations):
1955
2273
  transformationList.push(transformations);
@@ -1957,23 +2275,24 @@ var slice = [].slice,
1957
2275
  case !Util.isArray(transformations):
1958
2276
  resultArray = resultArray.concat(transformations);
1959
2277
  }
1960
- transformationList = ((function() {
1961
- var j, len, results;
2278
+ transformationList = (function() {
2279
+ var l, len1, results;
1962
2280
  results = [];
1963
- for (j = 0, len = transformationList.length; j < len; j++) {
1964
- value = transformationList[j];
2281
+ for (l = 0, len1 = transformationList.length; l < len1; l++) {
2282
+ value = transformationList[l];
1965
2283
  if (Util.isArray(value) && !Util.isEmpty(value) || !Util.isArray(value) && value) {
1966
2284
  results.push(value);
1967
2285
  }
1968
2286
  }
1969
2287
  return results;
1970
- })()).sort();
2288
+ })();
2289
+ transformationList = vars.sort().concat(variables).concat(transformationList.sort());
1971
2290
  if (ifParam === "if_end") {
1972
2291
  transformationList.push(ifParam);
1973
2292
  } else if (!Util.isEmpty(ifParam)) {
1974
2293
  transformationList.unshift(ifParam);
1975
2294
  }
1976
- transformationString = transformationList.join(this.param_separator);
2295
+ transformationString = Util.compact(transformationList).join(this.param_separator);
1977
2296
  if (!Util.isEmpty(transformationString)) {
1978
2297
  resultArray.push(transformationString);
1979
2298
  }
@@ -2060,6 +2379,20 @@ var slice = [].slice,
2060
2379
 
2061
2380
  TransformationBase.prototype.toString = function() {
2062
2381
  return this.serialize();
2382
+ };
2383
+
2384
+ processVar = function(varArray) {
2385
+ var j, len, name, ref, results, v;
2386
+ if (Util.isArray(varArray)) {
2387
+ results = [];
2388
+ for (j = 0, len = varArray.length; j < len; j++) {
2389
+ ref = varArray[j], name = ref[0], v = ref[1];
2390
+ results.push(name + "_" + (Expression.normalize(v)));
2391
+ }
2392
+ return results;
2393
+ } else {
2394
+ return varArray;
2395
+ }
2063
2396
 
2064
2397
  /**
2065
2398
  * Transformation Class methods.
@@ -2127,7 +2460,7 @@ var slice = [].slice,
2127
2460
  */
2128
2461
 
2129
2462
  Transformation.prototype.angle = function(value) {
2130
- return this.arrayParam(value, "angle", "a", ".");
2463
+ return this.arrayParam(value, "angle", "a", ".", Expression.normalize);
2131
2464
  };
2132
2465
 
2133
2466
  Transformation.prototype.audioCodec = function(value) {
@@ -2139,7 +2472,7 @@ var slice = [].slice,
2139
2472
  };
2140
2473
 
2141
2474
  Transformation.prototype.aspectRatio = function(value) {
2142
- return this.param(value, "aspect_ratio", "ar");
2475
+ return this.param(value, "aspect_ratio", "ar", Expression.normalize);
2143
2476
  };
2144
2477
 
2145
2478
  Transformation.prototype.background = function(value) {
@@ -2199,14 +2532,14 @@ var slice = [].slice,
2199
2532
  if (dpr != null ? dpr.match(/^\d+$/) : void 0) {
2200
2533
  return dpr + ".0";
2201
2534
  } else {
2202
- return dpr;
2535
+ return Expression.normalize(dpr);
2203
2536
  }
2204
2537
  };
2205
2538
  })(this));
2206
2539
  };
2207
2540
 
2208
2541
  Transformation.prototype.effect = function(value) {
2209
- return this.arrayParam(value, "effect", "e", ":");
2542
+ return this.arrayParam(value, "effect", "e", ":", Expression.normalize);
2210
2543
  };
2211
2544
 
2212
2545
  Transformation.prototype["else"] = function() {
@@ -2245,7 +2578,7 @@ var slice = [].slice,
2245
2578
  return this.param(value, "height", "h", (function(_this) {
2246
2579
  return function() {
2247
2580
  if (_this.getValue("crop") || _this.getValue("overlay") || _this.getValue("underlay")) {
2248
- return value;
2581
+ return Expression.normalize(value);
2249
2582
  } else {
2250
2583
  return null;
2251
2584
  }
@@ -2312,7 +2645,7 @@ var slice = [].slice,
2312
2645
  };
2313
2646
 
2314
2647
  Transformation.prototype.opacity = function(value) {
2315
- return this.param(value, "opacity", "o");
2648
+ return this.param(value, "opacity", "o", Expression.normalize);
2316
2649
  };
2317
2650
 
2318
2651
  Transformation.prototype.overlay = function(value) {
@@ -2332,11 +2665,11 @@ var slice = [].slice,
2332
2665
  };
2333
2666
 
2334
2667
  Transformation.prototype.quality = function(value) {
2335
- return this.param(value, "quality", "q");
2668
+ return this.param(value, "quality", "q", Expression.normalize);
2336
2669
  };
2337
2670
 
2338
2671
  Transformation.prototype.radius = function(value) {
2339
- return this.param(value, "radius", "r");
2672
+ return this.param(value, "radius", "r", Expression.normalize);
2340
2673
  };
2341
2674
 
2342
2675
  Transformation.prototype.rawTransformation = function(value) {
@@ -2376,6 +2709,14 @@ var slice = [].slice,
2376
2709
  return this.layerParam(value, "underlay", "u");
2377
2710
  };
2378
2711
 
2712
+ Transformation.prototype.variable = function(name, value) {
2713
+ return this.param(value, name, name);
2714
+ };
2715
+
2716
+ Transformation.prototype.variables = function(values) {
2717
+ return this.arrayParam(values, "variables");
2718
+ };
2719
+
2379
2720
  Transformation.prototype.videoCodec = function(value) {
2380
2721
  return this.param(value, "video_codec", "vc", Param.process_video_params);
2381
2722
  };
@@ -2388,7 +2729,7 @@ var slice = [].slice,
2388
2729
  return this.param(value, "width", "w", (function(_this) {
2389
2730
  return function() {
2390
2731
  if (_this.getValue("crop") || _this.getValue("overlay") || _this.getValue("underlay")) {
2391
- return value;
2732
+ return Expression.normalize(value);
2392
2733
  } else {
2393
2734
  return null;
2394
2735
  }
@@ -2397,15 +2738,15 @@ var slice = [].slice,
2397
2738
  };
2398
2739
 
2399
2740
  Transformation.prototype.x = function(value) {
2400
- return this.param(value, "x", "x");
2741
+ return this.param(value, "x", "x", Expression.normalize);
2401
2742
  };
2402
2743
 
2403
2744
  Transformation.prototype.y = function(value) {
2404
- return this.param(value, "y", "y");
2745
+ return this.param(value, "y", "y", Expression.normalize);
2405
2746
  };
2406
2747
 
2407
2748
  Transformation.prototype.zoom = function(value) {
2408
- return this.param(value, "zoom", "z");
2749
+ return this.param(value, "zoom", "z", Expression.normalize);
2409
2750
  };
2410
2751
 
2411
2752
  return Transformation;
@@ -2915,7 +3256,7 @@ var slice = [].slice,
2915
3256
  Cloudinary = (function() {
2916
3257
  var AKAMAI_SHARED_CDN, CF_SHARED_CDN, DEFAULT_POSTER_OPTIONS, DEFAULT_VIDEO_SOURCE_TYPES, OLD_AKAMAI_SHARED_CDN, SHARED_CDN, VERSION, absolutize, applyBreakpoints, cdnSubdomainNumber, closestAbove, cloudinaryUrlPrefix, defaultBreakpoints, finalizeResourceType, findContainerWidth, maxWidth, updateDpr;
2917
3258
 
2918
- VERSION = "2.1.9";
3259
+ VERSION = "2.3.0";
2919
3260
 
2920
3261
  CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net";
2921
3262
 
@@ -3601,7 +3942,8 @@ var slice = [].slice,
3601
3942
  imgOptions = new Transformation(imgOptions).toHtmlAttributes();
3602
3943
  Util.setData(node, 'src-cache', url);
3603
3944
  node.setAttribute('width', imgOptions.width);
3604
- results.push(node.setAttribute('height', imgOptions.height));
3945
+ node.setAttribute('height', imgOptions.height);
3946
+ results.push(node);
3605
3947
  }
3606
3948
  return results;
3607
3949
  }).call(this);
@@ -4150,7 +4492,7 @@ var slice = [].slice,
4150
4492
  TextLayer: TextLayer,
4151
4493
  SubtitlesLayer: SubtitlesLayer,
4152
4494
  Cloudinary: Cloudinary,
4153
- VERSION: "2.1.9",
4495
+ VERSION: "2.3.0",
4154
4496
  CloudinaryJQuery: CloudinaryJQuery
4155
4497
  };
4156
4498
  return cloudinary;