cloudinary 1.11.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  4. data/.github/pull_request_template.md +24 -0
  5. data/.gitignore +6 -1
  6. data/.travis.yml +15 -5
  7. data/CHANGELOG.md +149 -0
  8. data/Rakefile +3 -45
  9. data/cloudinary.gemspec +20 -4
  10. data/lib/active_storage/blob_key.rb +20 -0
  11. data/lib/active_storage/service/cloudinary_service.rb +229 -0
  12. data/lib/cloudinary.rb +31 -22
  13. data/lib/cloudinary/api.rb +173 -5
  14. data/lib/cloudinary/auth_token.rb +6 -4
  15. data/lib/cloudinary/carrier_wave.rb +3 -1
  16. data/lib/cloudinary/carrier_wave/remote.rb +3 -2
  17. data/lib/cloudinary/carrier_wave/storage.rb +2 -1
  18. data/lib/cloudinary/cloudinary_controller.rb +2 -4
  19. data/lib/cloudinary/helper.rb +30 -3
  20. data/lib/cloudinary/railtie.rb +7 -3
  21. data/lib/cloudinary/uploader.rb +35 -6
  22. data/lib/cloudinary/utils.rb +112 -40
  23. data/lib/cloudinary/version.rb +1 -1
  24. data/lib/cloudinary/video_helper.rb +96 -22
  25. data/lib/tasks/cloudinary/fetch_assets.rake +48 -0
  26. data/lib/tasks/{cloudinary.rake → cloudinary/sync_static.rake} +0 -0
  27. data/tools/allocate_test_cloud.sh +9 -0
  28. data/tools/get_test_cloud.sh +9 -0
  29. data/tools/update_version +29 -11
  30. data/vendor/assets/javascripts/cloudinary/jquery.cloudinary.js +48 -14
  31. data/vendor/assets/javascripts/cloudinary/jquery.fileupload.js +24 -4
  32. data/vendor/assets/javascripts/cloudinary/jquery.ui.widget.js +741 -561
  33. data/vendor/assets/javascripts/cloudinary/load-image.all.min.js +1 -1
  34. metadata +62 -67
  35. data/spec/access_control_spec.rb +0 -102
  36. data/spec/api_spec.rb +0 -567
  37. data/spec/archive_spec.rb +0 -129
  38. data/spec/auth_token_spec.rb +0 -77
  39. data/spec/cache_spec.rb +0 -109
  40. data/spec/cloudinary_helper_spec.rb +0 -325
  41. data/spec/cloudinary_spec.rb +0 -32
  42. data/spec/data/sync_static/app/assets/javascripts/1.coffee +0 -1
  43. data/spec/data/sync_static/app/assets/javascripts/1.js +0 -1
  44. data/spec/data/sync_static/app/assets/stylesheets/1.css +0 -3
  45. data/spec/docx.docx +0 -0
  46. data/spec/favicon.ico +0 -0
  47. data/spec/image_spec.rb +0 -107
  48. data/spec/logo.png +0 -0
  49. data/spec/rake_spec.rb +0 -160
  50. data/spec/sample_asset_file.tsv +0 -4
  51. data/spec/search_spec.rb +0 -109
  52. data/spec/spec_helper.rb +0 -266
  53. data/spec/storage_spec.rb +0 -44
  54. data/spec/streaminig_profiles_api_spec.rb +0 -74
  55. data/spec/support/helpers/temp_file_helpers.rb +0 -22
  56. data/spec/support/shared_contexts/rake.rb +0 -19
  57. data/spec/uploader_spec.rb +0 -392
  58. data/spec/utils_methods_spec.rb +0 -54
  59. data/spec/utils_spec.rb +0 -970
  60. data/spec/video_tag_spec.rb +0 -253
  61. data/spec/video_url_spec.rb +0 -185
@@ -29,8 +29,8 @@ module Cloudinary
29
29
  OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"
30
30
  SHARED_CDN = AKAMAI_SHARED_CDN
31
31
 
32
- USER_AGENT = "CloudinaryRuby/" + VERSION
33
- @@user_platform = ""
32
+ USER_AGENT = "CloudinaryRuby/#{VERSION} (Ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL})"
33
+ @@user_platform = defined?(Rails.version) ? "Rails/#{Rails.version}" : ""
34
34
 
35
35
  # Add platform information to the USER_AGENT header
36
36
  # This is intended for platform information and not individual applications!
@@ -44,7 +44,7 @@ module Cloudinary
44
44
 
45
45
  def self.USER_AGENT
46
46
  if @@user_platform.empty?
47
- "#{USER_AGENT}"
47
+ USER_AGENT
48
48
  else
49
49
  "#{@@user_platform} #{USER_AGENT}"
50
50
  end
@@ -64,19 +64,7 @@ module Cloudinary
64
64
  first_time = @@config.nil?
65
65
  @@config ||= OpenStruct.new((YAML.load(ERB.new(IO.read(config_dir.join("cloudinary.yml"))).result)[config_env] rescue {}))
66
66
 
67
- # Heroku support
68
- if first_time && ENV["CLOUDINARY_CLOUD_NAME"]
69
- set_config(
70
- "cloud_name" => ENV["CLOUDINARY_CLOUD_NAME"],
71
- "api_key" => ENV["CLOUDINARY_API_KEY"],
72
- "api_secret" => ENV["CLOUDINARY_API_SECRET"],
73
- "secure_distribution" => ENV["CLOUDINARY_SECURE_DISTRIBUTION"],
74
- "private_cdn" => ENV["CLOUDINARY_PRIVATE_CDN"].to_s == 'true',
75
- "secure" => ENV["CLOUDINARY_SECURE"].to_s == 'true'
76
- )
77
- elsif first_time && ENV["CLOUDINARY_URL"]
78
- config_from_url(ENV["CLOUDINARY_URL"])
79
- end
67
+ config_from_env if first_time
80
68
 
81
69
  set_config(new_config) if new_config
82
70
  yield(@@config) if block_given?
@@ -86,7 +74,12 @@ module Cloudinary
86
74
 
87
75
  def self.config_from_url(url)
88
76
  @@config ||= OpenStruct.new
89
- uri = URI.parse(url)
77
+ return unless url && !url.empty?
78
+ uri = URI.parse(url)
79
+ if !uri.scheme || "cloudinary" != uri.scheme.downcase
80
+ raise(CloudinaryException,
81
+ "Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'")
82
+ end
90
83
  set_config(
91
84
  "cloud_name" => uri.host,
92
85
  "api_key" => uri.user,
@@ -100,7 +93,7 @@ module Cloudinary
100
93
  if isNestedKey? key
101
94
  putNestedKey key, value
102
95
  else
103
- set_config(key => URI.decode(value))
96
+ set_config(key => Utils.smart_unescape(value))
104
97
  end
105
98
  end
106
99
  end
@@ -135,18 +128,34 @@ module Cloudinary
135
128
  end
136
129
 
137
130
  private
138
-
131
+
132
+ def self.config_from_env
133
+ # Heroku support
134
+ if ENV["CLOUDINARY_CLOUD_NAME"]
135
+ config_keys = ENV.keys.select! { |key| key.start_with? "CLOUDINARY_" }
136
+ config_keys -= ["CLOUDINARY_URL"] # ignore it when explicit options are passed
137
+ config_keys.each do |full_key|
138
+ conf_key = full_key["CLOUDINARY_".length..-1].downcase # convert "CLOUDINARY_CONFIG_NAME" to "config_name"
139
+ conf_val = ENV[full_key]
140
+ conf_val = conf_val == 'true' if %w[true false].include?(conf_val) # cast relevant boolean values
141
+ set_config(conf_key => conf_val)
142
+ end
143
+ elsif ENV["CLOUDINARY_URL"]
144
+ config_from_url(ENV["CLOUDINARY_URL"])
145
+ end
146
+ end
147
+
139
148
  def self.config_env
140
149
  return ENV["CLOUDINARY_ENV"] if ENV["CLOUDINARY_ENV"]
141
150
  return Rails.env if defined? Rails::env
142
151
  nil
143
152
  end
144
-
153
+
145
154
  def self.config_dir
146
- return Pathname.new(ENV["CLOUDINARY_CONFIG_DIR"]) if ENV["CLOUDINARY_CONFIG_DIR"]
155
+ return Pathname.new(ENV["CLOUDINARY_CONFIG_DIR"]) if ENV["CLOUDINARY_CONFIG_DIR"]
147
156
  self.app_root.join("config")
148
157
  end
149
-
158
+
150
159
  def self.set_config(new_config)
151
160
  new_config.each{|k,v| @@config.send(:"#{k}=", v) if !v.nil?}
152
161
  end
@@ -78,8 +78,9 @@ class Cloudinary::Api
78
78
  resource_type = options[:resource_type] || "image"
79
79
  type = options[:type] || "upload"
80
80
  uri = "resources/#{resource_type}/#{type}/#{public_id}"
81
- call_api(:get, uri,
82
- only(options,
81
+ call_api(:get, uri,
82
+ only(options,
83
+ :cinemagraph_analysis,
83
84
  :colors,
84
85
  :coordinates,
85
86
  :exif,
@@ -88,7 +89,9 @@ class Cloudinary::Api
88
89
  :max_results,
89
90
  :pages,
90
91
  :phash,
91
- :quality_analysis
92
+ :quality_analysis,
93
+ :derived_next_cursor,
94
+ :accessibility_analysis
92
95
  ), options)
93
96
  end
94
97
 
@@ -226,11 +229,21 @@ class Cloudinary::Api
226
229
  end
227
230
 
228
231
  def self.root_folders(options={})
229
- call_api(:get, "folders", {}, options)
232
+ params = only(options, :max_results, :next_cursor)
233
+ call_api(:get, "folders", params, options)
230
234
  end
231
235
 
232
236
  def self.subfolders(of_folder_path, options={})
233
- call_api(:get, "folders/#{of_folder_path}", {}, options)
237
+ params = only(options, :max_results, :next_cursor)
238
+ call_api(:get, "folders/#{of_folder_path}", params, options)
239
+ end
240
+
241
+ def self.delete_folder(path, options={})
242
+ call_api(:delete, "folders/#{path}", {}, options)
243
+ end
244
+
245
+ def self.create_folder(folder_name, options={})
246
+ call_api(:post, "folders/#{folder_name}", {}, options)
234
247
  end
235
248
 
236
249
  def self.upload_mappings(options={})
@@ -335,6 +348,144 @@ class Cloudinary::Api
335
348
  call_json_api('GET', json_url, {}, 60, {})
336
349
  end
337
350
 
351
+ # Returns a list of all metadata field definitions.
352
+ #
353
+ # @see https://cloudinary.com/documentation/admin_api#get_metadata_fields Get metadata fields API reference
354
+ #
355
+ # @param [Hash] options Additional options
356
+ # @return [Cloudinary::Api::Response]
357
+ # @raise [Cloudinary::Api::Error]
358
+ def self.list_metadata_fields(options = {})
359
+ call_metadata_api(:get, [], {}, options)
360
+ end
361
+
362
+ # Gets a metadata field by external id.
363
+ #
364
+ # @see https://cloudinary.com/documentation/admin_api#get_a_metadata_field_by_external_id Get metadata field by external ID API reference
365
+ #
366
+ # @param [String] field_external_id The ID of the metadata field to retrieve
367
+ # @param [Hash] options Additional options
368
+ # @return [Cloudinary::Api::Response]
369
+ # @raise [Cloudinary::Api::Error]
370
+ def self.metadata_field_by_field_id(field_external_id, options = {})
371
+ uri = [field_external_id]
372
+
373
+ call_metadata_api(:get, uri, {}, options)
374
+ end
375
+
376
+ # Creates a new metadata field definition.
377
+ #
378
+ # @see https://cloudinary.com/documentation/admin_api#create_a_metadata_field Create metadata field API reference
379
+ #
380
+ # @param [Hash] field The field to add
381
+ # @param [Hash] options Additional options
382
+ # @return [Cloudinary::Api::Response]
383
+ # @raise [Cloudinary::Api::Error]
384
+ def self.add_metadata_field(field, options = {})
385
+ params = only(field, :type, :external_id, :label, :mandatory, :default_value, :validation, :datasource)
386
+
387
+ call_metadata_api(:post, [], params, options)
388
+ end
389
+
390
+ # Updates a metadata field by external id.
391
+ #
392
+ # Updates a metadata field definition (partially, no need to pass the entire object) passed as JSON data.
393
+ # See https://cloudinary.com/documentation/admin_api#generic_structure_of_a_metadata_field for the generic structure
394
+ # of a metadata field.
395
+ #
396
+ # @see https://cloudinary.com/documentation/admin_api#update_a_metadata_field_by_external_id Update metadata field API reference
397
+ #
398
+ # @param [String] field_external_id The id of the metadata field to update
399
+ # @param [Hash] field The field definition
400
+ # @param [Hash] options Additional options
401
+ # @return [Cloudinary::Api::Response]
402
+ # @raise [Cloudinary::Api::Error]
403
+ def self.update_metadata_field(field_external_id, field, options = {})
404
+ uri = [field_external_id]
405
+ params = only(field, :label, :mandatory, :default_value, :validation)
406
+
407
+ call_metadata_api(:put, uri, params, options)
408
+ end
409
+
410
+ # Deletes a metadata field definition.
411
+ #
412
+ # The field should no longer be considered a valid candidate for all other endpoints.
413
+ #
414
+ # @see https://cloudinary.com/documentation/admin_api#delete_a_metadata_field_by_external_id Delete metadata field API reference
415
+ #
416
+ # @param [String] field_external_id The external id of the field to delete
417
+ # @param [Hash] options Additional options
418
+ # @return [Cloudinary::Api::Response] A hash with a "message" key. "ok" value indicates a successful deletion
419
+ # @raise [Cloudinary::Api::Error]
420
+ def self.delete_metadata_field(field_external_id, options = {})
421
+ uri = [field_external_id]
422
+
423
+ call_metadata_api(:delete, uri, {}, options)
424
+ end
425
+
426
+ # Deletes entries in a metadata field datasource.
427
+ #
428
+ # Deletes (blocks) the datasource entries for a specified metadata field definition. Sets the state of the
429
+ # entries to inactive. This is a soft delete, the entries still exist under the hood and can be activated
430
+ # again with the restore datasource entries method.
431
+ #
432
+ # @see https://cloudinary.com/documentation/admin_api#delete_entries_in_a_metadata_field_datasource Delete entries in a metadata field datasource API reference
433
+ #
434
+ # @param [String] field_external_id The id of the field to update
435
+ # @param [Array] entries_external_id The ids of all the entries to delete from the datasource
436
+ # @param [Hash] options Additional options
437
+ # @return [Cloudinary::Api::Response] The remaining datasource entries
438
+ # @raise [Cloudinary::Api::Error]
439
+ def self.delete_datasource_entries(field_external_id, entries_external_id, options = {})
440
+ uri = [field_external_id, "datasource"]
441
+ params = {:external_ids => entries_external_id }
442
+
443
+ call_metadata_api(:delete, uri, params, options)
444
+ end
445
+
446
+ # Updates a metadata field datasource.
447
+ #
448
+ # Updates the datasource of a supported field type (currently only enum and set), passed as JSON data. The
449
+ # update is partial: datasource entries with an existing external_id will be updated and entries with new
450
+ # external_id’s (or without external_id’s) will be appended.
451
+ #
452
+ # @see https://cloudinary.com/documentation/admin_api#update_a_metadata_field_datasource Update a metadata field datasource API reference
453
+ #
454
+ # @param [String] field_external_id The external id of the field to update
455
+ # @param [Array] entries_external_id
456
+ # @param [Hash] options Additional options
457
+ # @return [Cloudinary::Api::Response]
458
+ # @raise [Cloudinary::Api::Error]
459
+ def self.update_metadata_field_datasource(field_external_id, entries_external_id, options = {})
460
+ uri = [field_external_id, "datasource"]
461
+
462
+ params = entries_external_id.each_with_object({:values => [] }) do |item, hash|
463
+ item = only(item, :external_id, :value)
464
+ hash[:values ] << item if item.present?
465
+ end
466
+
467
+ call_metadata_api(:put, uri, params, options)
468
+ end
469
+
470
+ # Restores entries in a metadata field datasource.
471
+ #
472
+ # Restores (unblocks) any previously deleted datasource entries for a specified metadata field definition.
473
+ # Sets the state of the entries to active.
474
+ #
475
+ # @see https://cloudinary.com/documentation/admin_api#restore_entries_in_a_metadata_field_datasource Restore entries in a metadata field datasource API reference
476
+ #
477
+ # @param [String] field_external_id The ID of the metadata field
478
+ # @param [Array] entries_external_ids An array of IDs of datasource entries to restore (unblock)
479
+ # @param [Hash] options Additional options
480
+ # @return [Cloudinary::Api::Response]
481
+ # @raise [Cloudinary::Api::Error]
482
+ def self.restore_metadata_field_datasource(field_external_id, entries_external_ids, options = {})
483
+ uri = [field_external_id, "datasource_restore"]
484
+ params = {:external_ids => entries_external_ids }
485
+
486
+ call_metadata_api(:post, uri, params, options)
487
+ end
488
+
338
489
  protected
339
490
 
340
491
  def self.call_api(method, uri, params, options)
@@ -343,6 +494,7 @@ class Cloudinary::Api
343
494
  api_key = options[:api_key] || Cloudinary.config.api_key || raise("Must supply api_key")
344
495
  api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise("Must supply api_secret")
345
496
  timeout = options[:timeout] || Cloudinary.config.timeout || 60
497
+ uri = Cloudinary::Utils.smart_escape(uri)
346
498
  api_url = [cloudinary, "v1_1", cloud_name, uri].join("/")
347
499
  # Add authentication
348
500
  api_url.sub!(%r(^(https?://)), "\\1#{api_key}:#{api_secret}@")
@@ -382,6 +534,22 @@ class Cloudinary::Api
382
534
  raise GeneralError.new("Error parsing server response (#{response.code}) - #{response.body}. Got - #{e}")
383
535
  end
384
536
 
537
+ # Protected function that assists with performing an API call to the metadata_fields part of the Admin API.
538
+ #
539
+ # @protected
540
+ # @param [Symbol] method The HTTP method. Valid methods: get, post, put, delete
541
+ # @param [Array] uri REST endpoint of the API (without 'metadata_fields')
542
+ # @param [Hash] params Query/body parameters passed to the method
543
+ # @param [Hash] options Additional options. Can be an override of the configuration, headers, etc.
544
+ # @return [Cloudinary::Api::Response]
545
+ # @raise [Cloudinary::Api::Error]
546
+ def self.call_metadata_api(method, uri, params, options)
547
+ options[:content_type] = :json
548
+ uri = ["metadata_fields", uri].reject(&:empty?).join("/")
549
+
550
+ call_api(method, uri, params, options)
551
+ end
552
+
385
553
  def self.only(hash, *keys)
386
554
  result = {}
387
555
  keys.each do |key|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'openssl'
2
4
  if RUBY_VERSION > "2"
3
5
  require "ostruct"
@@ -10,6 +12,7 @@ module Cloudinary
10
12
  module AuthToken
11
13
  SEPARATOR = '~'
12
14
  UNSAFE = /[ "#%&\'\/:;<=>?@\[\\\]^`{\|}~]/
15
+ EMPTY_TOKEN = {}.freeze
13
16
 
14
17
  def self.generate(options = {})
15
18
  key = options[:key]
@@ -42,12 +45,11 @@ module Cloudinary
42
45
  "#{name}=#{token.join(SEPARATOR)}"
43
46
  end
44
47
 
45
-
46
48
  # Merge token2 to token1 returning a new
47
49
  # Requires to support Ruby 1.9
48
50
  def self.merge_auth_token(token1, token2)
49
- token1 = token1 || {}
50
- token2 = token2 || {}
51
+ token1 = token1 || EMPTY_TOKEN
52
+ token2 = token2 || EMPTY_TOKEN
51
53
  token1 = token1.respond_to?( :to_h) ? token1.to_h : token1
52
54
  token2 = token2.respond_to?( :to_h) ? token2.to_h : token2
53
55
  token1.merge(token2)
@@ -69,4 +71,4 @@ module Cloudinary
69
71
  OpenSSL::HMAC.hexdigest(digest, bin_key, message)
70
72
  end
71
73
  end
72
- end
74
+ end
@@ -9,8 +9,10 @@ module Cloudinary::CarrierWave
9
9
 
10
10
  def self.included(base)
11
11
  base.storage Cloudinary::CarrierWave::Storage
12
+ base.cache_storage = :file if base.cache_storage.blank?
12
13
  base.extend ClassMethods
13
- base.class_attribute :storage_type, :metadata
14
+ base.class_attribute :metadata
15
+ base.class_attribute :storage_type, instance_reader: false
14
16
  override_in_versions(base, :blank?, :full_public_id, :my_public_id, :all_versions_processors, :stored_version)
15
17
  end
16
18
 
@@ -1,10 +1,11 @@
1
1
  module Cloudinary::CarrierWave
2
2
  def download!(uri, *args)
3
- return super if !self.cloudinary_should_handle_remote?
3
+ return super unless self.cloudinary_should_handle_remote?
4
4
  if respond_to?(:process_uri)
5
5
  uri = process_uri(uri)
6
6
  else # Backward compatibility with old CarrierWave
7
- uri = URI.parse(URI.escape(URI.unescape(uri)))
7
+ remote_url_unsafe_chars = /([^a-zA-Z0-9_.\-\/:?&=]+)/ # In addition allow query string characters: "?","&" and "="
8
+ uri = URI.parse(Cloudinary::Utils.smart_escape(Cloudinary::Utils.smart_unescape(uri), remote_url_unsafe_chars))
8
9
  end
9
10
  return if uri.to_s.blank?
10
11
  self.original_filename = @cache_id = @filename = File.basename(uri.path).gsub(/[^a-zA-Z0-9\.\-\+_]/, '')
@@ -1,7 +1,8 @@
1
1
  class Cloudinary::CarrierWave::Storage < ::CarrierWave::Storage::Abstract
2
2
 
3
3
  def store!(file)
4
- return if !uploader.enable_processing
4
+ return unless uploader.enable_processing
5
+
5
6
  if uploader.is_main_uploader?
6
7
  case file
7
8
  when Cloudinary::CarrierWave::PreloadedCloudinaryFile
@@ -1,13 +1,11 @@
1
1
  module Cloudinary::CloudinaryController
2
2
  protected
3
-
3
+
4
4
  def valid_cloudinary_response?
5
5
  received_signature = request.query_parameters[:signature]
6
6
  calculated_signature = Cloudinary::Utils.api_sign_request(
7
7
  request.query_parameters.select{|key, value| [:public_id, :version].include?(key.to_sym)},
8
8
  Cloudinary.config.api_secret)
9
9
  return received_signature == calculated_signature
10
- end
10
+ end
11
11
  end
12
-
13
- ActionController::Base.send :include, Cloudinary::CloudinaryController
@@ -286,7 +286,7 @@ module CloudinaryHelper
286
286
  Cloudinary::Utils.private_download_url(public_id, format, options)
287
287
  end
288
288
 
289
- # Helper method that uses the deprecated ZIP download API.
289
+ # Helper method that uses the deprecated ZIP download API.
290
290
  # Replaced by cl_download_zip_url that uses the more advanced and robust archive generation and download API
291
291
  # @deprecated
292
292
  def cl_zip_download_url(tag, options = {})
@@ -296,7 +296,7 @@ module CloudinaryHelper
296
296
  # @see {Cloudinary::Utils.download_archive_url}
297
297
  def cl_download_archive_url(options = {})
298
298
  Cloudinary::Utils.download_archive_url(options)
299
- end
299
+ end
300
300
 
301
301
  # @see {Cloudinary::Utils.download_zip_url}
302
302
  def cl_download_zip_url(options = {})
@@ -318,7 +318,7 @@ module CloudinaryHelper
318
318
  if Cloudinary.config.enhance_image_tag
319
319
  alias_method :image_tag, :image_tag_with_cloudinary
320
320
  alias_method :image_path, :image_path_with_cloudinary
321
- end
321
+ end
322
322
  end
323
323
  end
324
324
 
@@ -419,3 +419,30 @@ rescue LoadError
419
419
  # no sass support. Ignore.
420
420
  end
421
421
 
422
+ begin
423
+ require 'sassc'
424
+ require 'sassc/script/functions'
425
+ module SassC::Script::Functions
426
+ # Helper method for generating cloudinary_url in scss files.
427
+ #
428
+ # As opposed to sass(deprecated), optional named arguments are not supported, use hash map instead.
429
+ #
430
+ # Example:
431
+ # Sass: cloudinary-url("sample", $quality: "auto", $fetch_format: "auto");
432
+ # becomes
433
+ # SassC: cloudinary-url("sample", ("quality": "auto", "fetch_format": "auto"));
434
+ #
435
+ # @param [::SassC::Script::Value::String] public_id The public ID of the resource
436
+ # @param [::SassC::Script::Value::Map] sass_options Additional options
437
+ #
438
+ # @return [::SassC::Script::Value::String]
439
+ def cloudinary_url(public_id, sass_options = {})
440
+ options = {}
441
+ sass_options.to_h.each { |k, v| options[k.value.to_sym] = v.value }
442
+ url = Cloudinary::Utils.cloudinary_url(public_id.value, {:type => :asset}.merge(options))
443
+ ::SassC::Script::Value::String.new("url(#{url})")
444
+ end
445
+ end
446
+ rescue LoadError
447
+ # no sassc support. Ignore.
448
+ end