cdnget 0.3.1 → 1.1.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.
- checksums.yaml +4 -4
- data/CHANGES.md +40 -0
- data/MIT-LICENSE +1 -1
- data/README.md +33 -11
- data/Rakefile +2 -2
- data/bin/cdnget +471 -142
- data/cdnget.gemspec +4 -5
- data/lib/cdnget.rb +471 -142
- data/test/cdnget_test.rb +1061 -216
- metadata +7 -21
    
        data/lib/cdnget.rb
    CHANGED
    
    | @@ -1,31 +1,109 @@ | |
| 1 1 | 
             
            #!/usr/bin/env ruby
         | 
| 2 2 | 
             
            # -*- coding: utf-8 -*-
         | 
| 3 | 
            +
            # frozen_string_literal: true
         | 
| 3 4 |  | 
| 4 5 | 
             
            ##
         | 
| 5 | 
            -
            ## Download files from CDN | 
| 6 | 
            +
            ## Download JS/CSS files from public CDN.
         | 
| 6 7 | 
             
            ##
         | 
| 7 | 
            -
            ##  | 
| 8 | 
            -
            ##  | 
| 9 | 
            -
            ##  | 
| 8 | 
            +
            ## * CDNJS    (https://cdnjs.com/)
         | 
| 9 | 
            +
            ## * jsDelivr (https://www.jsdelivr.com/)
         | 
| 10 | 
            +
            ## * UNPKG    (https://unpkg.com/)
         | 
| 11 | 
            +
            ## * Google   (https://developers.google.com/speed/libraries/)
         | 
| 10 12 | 
             
            ##
         | 
| 11 13 | 
             
            ## Example:
         | 
| 12 14 | 
             
            ##  $ cdnget                                # list public CDN
         | 
| 13 | 
            -
            ##  $ cdnget [-q] cdnjs                     # list libraries
         | 
| 15 | 
            +
            ##  $ cdnget [-q] cdnjs                     # list libraries (except jsdelivr/unpkg)
         | 
| 16 | 
            +
            ##  $ cdnget [-q] cdnjs '*jquery*'          # search libraries
         | 
| 14 17 | 
             
            ##  $ cdnget [-q] cdnjs jquery              # list versions
         | 
| 15 | 
            -
            ##  $ cdnget [-q] cdnjs jquery  | 
| 16 | 
            -
            ##  $ cdnget [-q] cdnjs jquery  | 
| 18 | 
            +
            ##  $ cdnget [-q] cdnjs jquery latest       # detect latest version
         | 
| 19 | 
            +
            ##  $ cdnget [-q] cdnjs jquery 3.6.0        # list files
         | 
| 20 | 
            +
            ##  $ mkdir -p static/lib                   # create a directory
         | 
| 21 | 
            +
            ##  $ cdnget [-q] cdnjs jquery 3.6.0 static/lib  # download files
         | 
| 17 22 | 
             
            ##
         | 
| 18 23 |  | 
| 19 24 | 
             
            require 'open-uri'
         | 
| 20 25 | 
             
            require 'uri'
         | 
| 26 | 
            +
            require 'net/http'
         | 
| 27 | 
            +
            require 'openssl'
         | 
| 21 28 | 
             
            require 'json'
         | 
| 22 29 | 
             
            require 'fileutils'
         | 
| 30 | 
            +
            require 'pp'
         | 
| 23 31 |  | 
| 24 32 |  | 
| 25 33 | 
             
            module CDNGet
         | 
| 26 34 |  | 
| 27 35 |  | 
| 28 | 
            -
              RELEASE = '$Release:  | 
| 36 | 
            +
              RELEASE = '$Release: 1.1.0 $'.split()[1]
         | 
| 37 | 
            +
             | 
| 38 | 
            +
             | 
| 39 | 
            +
              class HttpConnection
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def initialize(uri, headers=nil)
         | 
| 42 | 
            +
                  http = Net::HTTP.new(uri.host, uri.port)
         | 
| 43 | 
            +
                  if uri.scheme == 'https'
         | 
| 44 | 
            +
                    http.use_ssl = true
         | 
| 45 | 
            +
                    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                  http.start()
         | 
| 48 | 
            +
                  @http = http
         | 
| 49 | 
            +
                  @headers = headers
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def self.open(uri, headers=nil)
         | 
| 53 | 
            +
                  http = self.new(uri, headers)
         | 
| 54 | 
            +
                  return http unless block_given?()
         | 
| 55 | 
            +
                  begin
         | 
| 56 | 
            +
                    return yield http
         | 
| 57 | 
            +
                  ensure
         | 
| 58 | 
            +
                    http.close()
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def get(uri)
         | 
| 63 | 
            +
                  resp = request('GET', uri.path, uri.query)
         | 
| 64 | 
            +
                  return _get_resp_body(resp)
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def post(uri, payload)
         | 
| 68 | 
            +
                  resp = request('POST', uri.path, uri.query, payload: payload)
         | 
| 69 | 
            +
                  return _get_resp_body(resp)
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def request(meth, path, query=nil, payload: nil, headers: nil)
         | 
| 73 | 
            +
                  path += "?" + query if query
         | 
| 74 | 
            +
                  if @headers
         | 
| 75 | 
            +
                    headers ||= {}
         | 
| 76 | 
            +
                    headers.update(@headers)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
                  resp = @http.send_request(meth, path, payload, headers)
         | 
| 79 | 
            +
                  return resp
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def _get_resp_body(resp)
         | 
| 83 | 
            +
                  case resp
         | 
| 84 | 
            +
                  when Net::HTTPSuccess
         | 
| 85 | 
            +
                    return resp.body
         | 
| 86 | 
            +
                  else
         | 
| 87 | 
            +
                    raise HttpError.new(resp.code.to_i, resp.message)
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def close()
         | 
| 92 | 
            +
                  @http.finish()
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
             | 
| 98 | 
            +
              class HttpError < StandardError
         | 
| 99 | 
            +
                def initialize(code, msgtext)
         | 
| 100 | 
            +
                  super("#{code} #{msgtext}")
         | 
| 101 | 
            +
                  @code = code
         | 
| 102 | 
            +
                  @msgtext = msgtext
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
                attr_reader :code, :msgtext
         | 
| 105 | 
            +
              end
         | 
| 106 | 
            +
             | 
| 29 107 |  | 
| 30 108 | 
             
              CLASSES = []
         | 
| 31 109 |  | 
| @@ -36,10 +114,22 @@ module CDNGet | |
| 36 114 | 
             
                  CLASSES << klass
         | 
| 37 115 | 
             
                end
         | 
| 38 116 |  | 
| 117 | 
            +
                def initialize(debug_mode: false)
         | 
| 118 | 
            +
                  @debug_mode = debug_mode
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
                attr_reader :debug_mode
         | 
| 121 | 
            +
             | 
| 39 122 | 
             
                def list()
         | 
| 40 123 | 
             
                  raise NotImplementedError.new("#{self.class.name}#list(): not implemented yet.")
         | 
| 41 124 | 
             
                end
         | 
| 42 125 |  | 
| 126 | 
            +
                def search(pattern)
         | 
| 127 | 
            +
                  return list().select {|d| File.fnmatch(pattern, d[:name], File::FNM_CASEFOLD) }
         | 
| 128 | 
            +
                  #rexp_str = pattern.split('*', -1).collect {|x| Regexp.escape(x) }.join('.*')
         | 
| 129 | 
            +
                  #rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
         | 
| 130 | 
            +
                  #return list().select {|d| d[:name] =~ rexp }
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 43 133 | 
             
                def find(library)
         | 
| 44 134 | 
             
                  raise NotImplementedError.new("#{self.class.name}#find(): not implemented yet.")
         | 
| 45 135 | 
             
                end
         | 
| @@ -54,14 +144,36 @@ module CDNGet | |
| 54 144 | 
             
                    raise CommandError.new("#{basedir}: not exist.")
         | 
| 55 145 | 
             
                  File.directory?(basedir)  or
         | 
| 56 146 | 
             
                    raise CommandError.new("#{basedir}: not a directory.")
         | 
| 57 | 
            -
                  target_dir = File.join(basedir, library, version)
         | 
| 58 147 | 
             
                  d = get(library, version)
         | 
| 148 | 
            +
                  target_dir = d[:destdir] ? File.join(basedir, d[:destdir]) \
         | 
| 149 | 
            +
                                           : File.join(basedir, library, version)
         | 
| 150 | 
            +
                  http = nil
         | 
| 151 | 
            +
                  skipfile = d[:skipfile]   # ex: /\.DS_Store\z/
         | 
| 59 152 | 
             
                  d[:files].each do |file|
         | 
| 60 153 | 
             
                    filepath = File.join(target_dir, file)
         | 
| 154 | 
            +
                    #
         | 
| 155 | 
            +
                    if skipfile && file =~ skipfile
         | 
| 156 | 
            +
                      puts "#{filepath} ... Skipped"   # for example, skip '.DS_Store' files
         | 
| 157 | 
            +
                      next
         | 
| 158 | 
            +
                    end
         | 
| 159 | 
            +
                    #
         | 
| 160 | 
            +
                    if filepath.end_with?('/')
         | 
| 161 | 
            +
                      if File.exist?(filepath)
         | 
| 162 | 
            +
                        puts "#{filepath} ... Done (Already exists)" unless quiet
         | 
| 163 | 
            +
                      else
         | 
| 164 | 
            +
                        print "#{filepath} ..." unless quiet
         | 
| 165 | 
            +
                        FileUtils.mkdir_p(filepath)
         | 
| 166 | 
            +
                        puts " Done (Created)" unless quiet
         | 
| 167 | 
            +
                      end
         | 
| 168 | 
            +
                      next
         | 
| 169 | 
            +
                    end
         | 
| 170 | 
            +
                    #
         | 
| 61 171 | 
             
                    dirpath  = File.dirname(filepath)
         | 
| 62 172 | 
             
                    print "#{filepath} ..." unless quiet
         | 
| 63 173 | 
             
                    url = File.join(d[:baseurl], file)   # not use URI.join!
         | 
| 64 | 
            -
                     | 
| 174 | 
            +
                    uri = URI.parse(url)
         | 
| 175 | 
            +
                    http ||= HttpConnection.new(uri)
         | 
| 176 | 
            +
                    content = http.get(uri)
         | 
| 65 177 | 
             
                    content = content.force_encoding('ascii-8bit')
         | 
| 66 178 | 
             
                    print " Done (#{format_integer(content.bytesize)} byte)" unless quiet
         | 
| 67 179 | 
             
                    FileUtils.mkdir_p(dirpath) unless File.exist?(dirpath)
         | 
| @@ -73,56 +185,83 @@ module CDNGet | |
| 73 185 | 
             
                    end
         | 
| 74 186 | 
             
                    puts() unless quiet
         | 
| 75 187 | 
             
                  end
         | 
| 188 | 
            +
                  http.close() if http
         | 
| 76 189 | 
             
                  nil
         | 
| 77 190 | 
             
                end
         | 
| 78 191 |  | 
| 192 | 
            +
                def latest_version(library)
         | 
| 193 | 
            +
                  validate(library, nil)
         | 
| 194 | 
            +
                  d = self.find(library)
         | 
| 195 | 
            +
                  return d[:versions].first
         | 
| 196 | 
            +
                end
         | 
| 197 | 
            +
             | 
| 79 198 | 
             
                protected
         | 
| 80 199 |  | 
| 200 | 
            +
                def http_get(url)
         | 
| 201 | 
            +
                  ## * `open()` on Ruby 3.X can't open http url
         | 
| 202 | 
            +
                  ## * `URI.open()` on Ruby <= 2.4 raises NoMethodError (private method `open' called)
         | 
| 203 | 
            +
                  ## * `URI.__send__(:open)` is a hack to work on both Ruby 2.X and 3.X
         | 
| 204 | 
            +
                  return URI.__send__(:open, url, 'rb') {|f| f.read() }
         | 
| 205 | 
            +
                end
         | 
| 206 | 
            +
             | 
| 81 207 | 
             
                def fetch(url, library=nil)
         | 
| 82 208 | 
             
                  begin
         | 
| 83 | 
            -
                    html =  | 
| 209 | 
            +
                    html = http_get(url)
         | 
| 84 210 | 
             
                    return html
         | 
| 85 | 
            -
                  rescue OpenURI::HTTPError =>  | 
| 86 | 
            -
                     | 
| 211 | 
            +
                  rescue OpenURI::HTTPError => exc
         | 
| 212 | 
            +
                    if ! (exc.message == "404 Not Found" && library)
         | 
| 213 | 
            +
                      raise CommandError.new("GET #{url} : #{exc.message}")
         | 
| 214 | 
            +
                    elsif ! library.end_with?('js')
         | 
| 215 | 
            +
                      raise CommandError.new("#{library}: Library not found.")
         | 
| 216 | 
            +
                    else
         | 
| 217 | 
            +
                      maybe = library.end_with?('.js') ? library.sub('.js', 'js') : library.sub(/js$/, '.js')
         | 
| 218 | 
            +
                      raise CommandError.new("#{library}: Library not found (maybe '#{maybe}'?).")
         | 
| 219 | 
            +
                    end
         | 
| 87 220 | 
             
                  end
         | 
| 88 221 | 
             
                end
         | 
| 89 222 |  | 
| 223 | 
            +
                LIBRARY_REXP = /\A[-.\w]+\z/
         | 
| 224 | 
            +
                VERSION_REXP = /\A\d+(\.\d+)+([-.\w]+)?/
         | 
| 225 | 
            +
             | 
| 90 226 | 
             
                def validate(library, version)
         | 
| 91 227 | 
             
                  if library
         | 
| 92 | 
            -
                     | 
| 93 | 
            -
             | 
| 228 | 
            +
                    rexp = self.class.const_get(:LIBRARY_REXP)
         | 
| 229 | 
            +
                    library =~ self.class.const_get(:LIBRARY_REXP)  or
         | 
| 230 | 
            +
                      raise CommandError.new("#{library}: Invalid library name.")
         | 
| 94 231 | 
             
                  end
         | 
| 95 232 | 
             
                  if version
         | 
| 96 | 
            -
                    version =~  | 
| 97 | 
            -
                      raise  | 
| 233 | 
            +
                    version =~ self.class.const_get(:VERSION_REXP)  or
         | 
| 234 | 
            +
                      raise CommandError.new("#{version}: Invalid version number.")
         | 
| 98 235 | 
             
                  end
         | 
| 99 236 | 
             
                end
         | 
| 100 237 |  | 
| 238 | 
            +
                def npmpkg_url(library, version)
         | 
| 239 | 
            +
                  pkg  = library.sub(/^@[-\w]+/, '')
         | 
| 240 | 
            +
                  path = "/#{library.gsub('/', '%2f')}/-/#{pkg}-#{version}.tgz"
         | 
| 241 | 
            +
                  return "https://registry.npmjs.org#{path}"
         | 
| 242 | 
            +
                end
         | 
| 243 | 
            +
             | 
| 101 244 | 
             
                def format_integer(value)
         | 
| 102 245 | 
             
                  return value.to_s.reverse.scan(/..?.?/).collect {|s| s.reverse }.reverse.join(',')
         | 
| 103 246 | 
             
                end
         | 
| 104 247 |  | 
| 105 | 
            -
             | 
| 106 | 
            -
             | 
| 248 | 
            +
                def _debug_print(x)
         | 
| 249 | 
            +
                  if @debug_mode
         | 
| 250 | 
            +
                    $stderr.puts "\e[0;35m*** #{PP.pp(x,String.new)}\e[0m"
         | 
| 251 | 
            +
                  end
         | 
| 252 | 
            +
                end
         | 
| 107 253 |  | 
| 108 | 
            -
              class HttpError < StandardError
         | 
| 109 254 | 
             
              end
         | 
| 110 255 |  | 
| 111 256 |  | 
| 112 | 
            -
              class CDNJS < Base | 
| 257 | 
            +
              class CDNJS < Base
         | 
| 113 258 | 
             
                CODE = "cdnjs"
         | 
| 114 | 
            -
                SITE_URL =  | 
| 259 | 
            +
                SITE_URL = "https://cdnjs.com/"
         | 
| 260 | 
            +
                API_URL  = "https://api.cdnjs.com/libraries"
         | 
| 261 | 
            +
                CDN_URL  = "https://cdnjs.cloudflare.com/ajax/libs"
         | 
| 115 262 |  | 
| 116 263 | 
             
                def fetch(url, library=nil)
         | 
| 117 | 
            -
                   | 
| 118 | 
            -
                    json_str = URI.open(url, 'rb') {|f| f.read() }
         | 
| 119 | 
            -
                  rescue OpenURI::HTTPError => exc
         | 
| 120 | 
            -
                    if exc.message == "404 Not Found"
         | 
| 121 | 
            -
                      json_str = "{}"
         | 
| 122 | 
            -
                    else
         | 
| 123 | 
            -
                      raise HttpError.new("GET #{url} : #{ex.message}")
         | 
| 124 | 
            -
                    end
         | 
| 125 | 
            -
                  end
         | 
| 264 | 
            +
                  json_str = super
         | 
| 126 265 | 
             
                  if json_str == "{}" && library
         | 
| 127 266 | 
             
                    if library.end_with?('js')
         | 
| 128 267 | 
             
                      maybe = library.end_with?('.js') \
         | 
| @@ -137,43 +276,51 @@ module CDNGet | |
| 137 276 | 
             
                end
         | 
| 138 277 | 
             
                protected :fetch
         | 
| 139 278 |  | 
| 140 | 
            -
                def list
         | 
| 141 | 
            -
                   | 
| 142 | 
            -
                  jstr = fetch("https://api.cdnjs.com/libraries?fields=name,description")
         | 
| 279 | 
            +
                def list()
         | 
| 280 | 
            +
                  jstr = fetch("#{API_URL}?fields=name,description")
         | 
| 143 281 | 
             
                  jdata = JSON.parse(jstr)
         | 
| 282 | 
            +
                  _debug_print(jdata)
         | 
| 144 283 | 
             
                  libs = jdata['results'].collect {|d| {name: d['name'], desc: d['description']} }
         | 
| 145 284 | 
             
                  return libs.sort_by {|d| d[:name] }.uniq
         | 
| 146 285 | 
             
                end
         | 
| 147 286 |  | 
| 148 287 | 
             
                def find(library)
         | 
| 149 288 | 
             
                  validate(library, nil)
         | 
| 150 | 
            -
                  jstr = fetch(" | 
| 289 | 
            +
                  jstr = fetch("#{API_URL}/#{library}", library)
         | 
| 151 290 | 
             
                  jdata = JSON.parse(jstr)
         | 
| 291 | 
            +
                  _debug_print(jdata)
         | 
| 152 292 | 
             
                  versions = jdata['assets'].collect {|d| d['version'] }\
         | 
| 153 293 | 
             
                               .sort_by {|v| v.split(/[-.]/).map(&:to_i) }
         | 
| 154 294 | 
             
                  return {
         | 
| 155 295 | 
             
                    name: library,
         | 
| 156 296 | 
             
                    desc: jdata['description'],
         | 
| 157 297 | 
             
                    tags: (jdata['keywords'] || []).join(", "),
         | 
| 298 | 
            +
                    site: jdata['homepage'],
         | 
| 299 | 
            +
                    info: File.join(SITE_URL, "/libraries/#{library}"),
         | 
| 300 | 
            +
                    license: jdata['license'],
         | 
| 158 301 | 
             
                    versions: versions.reverse(),
         | 
| 159 302 | 
             
                  }
         | 
| 160 303 | 
             
                end
         | 
| 161 304 |  | 
| 162 305 | 
             
                def get(library, version)
         | 
| 163 306 | 
             
                  validate(library, version)
         | 
| 164 | 
            -
                  jstr = fetch(" | 
| 307 | 
            +
                  jstr = fetch("#{API_URL}/#{library}", library)
         | 
| 165 308 | 
             
                  jdata = JSON.parse(jstr)
         | 
| 309 | 
            +
                  _debug_print(jdata)
         | 
| 166 310 | 
             
                  d = jdata['assets'].find {|d| d['version'] == version }  or
         | 
| 167 311 | 
             
                    raise CommandError.new("#{library}/#{version}: Library or version not found.")
         | 
| 168 | 
            -
                  baseurl = " | 
| 312 | 
            +
                  baseurl = "#{CDN_URL}/#{library}/#{version}/"
         | 
| 169 313 | 
             
                  return {
         | 
| 170 314 | 
             
                    name:     library,
         | 
| 315 | 
            +
                    version:  version,
         | 
| 171 316 | 
             
                    desc:     jdata['description'],
         | 
| 172 317 | 
             
                    tags:     (jdata['keywords'] || []).join(", "),
         | 
| 173 | 
            -
                     | 
| 318 | 
            +
                    site:     jdata['homepage'],
         | 
| 319 | 
            +
                    info:     File.join(SITE_URL, "/libraries/#{library}/#{version}"),
         | 
| 174 320 | 
             
                    urls:     d['files'].collect {|s| baseurl + s },
         | 
| 175 321 | 
             
                    files:    d['files'],
         | 
| 176 322 | 
             
                    baseurl:  baseurl,
         | 
| 323 | 
            +
                    license:  jdata['license'],
         | 
| 177 324 | 
             
                  }
         | 
| 178 325 | 
             
                end
         | 
| 179 326 |  | 
| @@ -183,59 +330,237 @@ module CDNGet | |
| 183 330 | 
             
              class JSDelivr < Base
         | 
| 184 331 | 
             
                CODE = "jsdelivr"
         | 
| 185 332 | 
             
                SITE_URL = "https://www.jsdelivr.com/"
         | 
| 186 | 
            -
                API_URL  = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
         | 
| 333 | 
            +
                #API_URL  = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
         | 
| 334 | 
            +
                API_URL  = "https://data.jsdelivr.com/v1"
         | 
| 335 | 
            +
                CDN_URL  = "https://cdn.jsdelivr.net/npm"
         | 
| 336 | 
            +
                HEADERS = {
         | 
| 337 | 
            +
                  "x-algo""lia-app""lication-id"=>"OFCNC""OG2CU",
         | 
| 338 | 
            +
                  "x-algo""lia-api""-key"=>"f54e21fa3a2""a0160595bb05""8179bfb1e",
         | 
| 339 | 
            +
                }
         | 
| 340 | 
            +
                LIBRARY_REXP = /\A([-.\w]+|\@[-\w]+\/[-.\w]+)\z/
         | 
| 341 | 
            +
             | 
| 342 | 
            +
                def list()
         | 
| 343 | 
            +
                  return nil    # nil means that this CDN can't list libraries without pattern
         | 
| 344 | 
            +
                end
         | 
| 187 345 |  | 
| 188 | 
            -
                def  | 
| 189 | 
            -
                   | 
| 190 | 
            -
             | 
| 191 | 
            -
             | 
| 192 | 
            -
                     | 
| 346 | 
            +
                def search(pattern)
         | 
| 347 | 
            +
                  form_data = {
         | 
| 348 | 
            +
                    query:        pattern,
         | 
| 349 | 
            +
                    page:         "0",
         | 
| 350 | 
            +
                    hitsPerPage:  "1000",
         | 
| 351 | 
            +
                    attributesToHighlight: '[]',
         | 
| 352 | 
            +
                    attributesToRetrieve:  '["name","description","version"]'
         | 
| 353 | 
            +
                  }
         | 
| 354 | 
            +
                  payload = JSON.dump({"params"=>URI.encode_www_form(form_data)})
         | 
| 355 | 
            +
                  url = "https://ofcncog2cu-3.algolianet.com/1/indexes/npm-search/query"
         | 
| 356 | 
            +
                  uri = URI.parse(url)
         | 
| 357 | 
            +
                  json = HttpConnection.open(uri, HEADERS) {|http| http.post(uri, payload) }
         | 
| 358 | 
            +
                  jdata = JSON.load(json)
         | 
| 359 | 
            +
                  _debug_print(jdata)
         | 
| 360 | 
            +
                  return jdata["hits"].select {|d|
         | 
| 361 | 
            +
                    File.fnmatch(pattern, d["name"], File::FNM_CASEFOLD)
         | 
| 362 | 
            +
                  }.collect {|d|
         | 
| 363 | 
            +
                    {name: d["name"], desc: d["description"], version: d["version"]}
         | 
| 193 364 | 
             
                  }
         | 
| 194 365 | 
             
                end
         | 
| 195 366 |  | 
| 196 367 | 
             
                def find(library)
         | 
| 197 368 | 
             
                  validate(library, nil)
         | 
| 198 | 
            -
                   | 
| 199 | 
            -
                   | 
| 200 | 
            -
                   | 
| 201 | 
            -
                     | 
| 369 | 
            +
                  url = "https://ofcncog2cu-dsn.algolia.net/1/indexes/npm-search/#{library.sub('/', '%2f')}"
         | 
| 370 | 
            +
                  uri = URI.parse(url)
         | 
| 371 | 
            +
                  begin
         | 
| 372 | 
            +
                    json = HttpConnection.open(uri, HEADERS) {|http| http.get(uri) }
         | 
| 373 | 
            +
                  rescue HttpError
         | 
| 374 | 
            +
                    raise CommandError, "#{library}: Library not found."
         | 
| 375 | 
            +
                  end
         | 
| 376 | 
            +
                  dict1 = JSON.load(json)
         | 
| 377 | 
            +
                  _debug_print(dict1)
         | 
| 378 | 
            +
                  #
         | 
| 379 | 
            +
                  json = fetch("#{API_URL}/package/npm/#{library}")
         | 
| 380 | 
            +
                  dict2 = JSON.load(json)
         | 
| 381 | 
            +
                  _debug_print(dict2)
         | 
| 382 | 
            +
                  #
         | 
| 383 | 
            +
                  d = dict1
         | 
| 202 384 | 
             
                  return {
         | 
| 203 | 
            -
                    name: | 
| 204 | 
            -
                    desc: | 
| 205 | 
            -
                     | 
| 206 | 
            -
                    versions: | 
| 385 | 
            +
                    name:      d['name'],
         | 
| 386 | 
            +
                    desc:      d['description'],
         | 
| 387 | 
            +
                    #versions: d['versions'].collect {|k,v| k },
         | 
| 388 | 
            +
                    versions:  dict2['versions'],
         | 
| 389 | 
            +
                    tags:      (d['keywords'] || []).join(", "),
         | 
| 390 | 
            +
                    site:      d['homepage'],
         | 
| 391 | 
            +
                    info:      File.join(SITE_URL, "/package/npm/#{library}"),
         | 
| 392 | 
            +
                    license:   d['license'],
         | 
| 207 393 | 
             
                  }
         | 
| 208 394 | 
             
                end
         | 
| 209 395 |  | 
| 210 396 | 
             
                def get(library, version)
         | 
| 211 397 | 
             
                  validate(library, version)
         | 
| 212 | 
            -
                   | 
| 213 | 
            -
                   | 
| 214 | 
            -
             | 
| 215 | 
            -
                   | 
| 216 | 
            -
             | 
| 217 | 
            -
             | 
| 218 | 
            -
                   | 
| 398 | 
            +
                  url = File.join(API_URL, "/package/npm/#{library}@#{version}/flat")
         | 
| 399 | 
            +
                  begin
         | 
| 400 | 
            +
                    json = fetch(url, library)
         | 
| 401 | 
            +
                  rescue CommandError
         | 
| 402 | 
            +
                    raise CommandError.new("#{library}@#{version}: Library or version not found.")
         | 
| 403 | 
            +
                  end
         | 
| 404 | 
            +
                  jdata   = JSON.load(json)
         | 
| 405 | 
            +
                  files   = jdata["files"].collect {|d| d["name"] }
         | 
| 406 | 
            +
                  baseurl = "#{CDN_URL}/#{library}@#{version}"
         | 
| 407 | 
            +
                  _debug_print(jdata)
         | 
| 408 | 
            +
                  #
         | 
| 409 | 
            +
                  dict = find(library)
         | 
| 410 | 
            +
                  dict.delete(:versions)
         | 
| 411 | 
            +
                  dict.update({
         | 
| 412 | 
            +
                    version: version,
         | 
| 413 | 
            +
                    info:    File.join(SITE_URL, "/package/npm/#{library}?version=#{version}"),
         | 
| 414 | 
            +
                    npmpkg:  npmpkg_url(library, version),
         | 
| 415 | 
            +
                    urls:    files.collect {|x| baseurl + x },
         | 
| 416 | 
            +
                    files:   files,
         | 
| 417 | 
            +
                    baseurl: baseurl,
         | 
| 418 | 
            +
                    default: jdata["default"],
         | 
| 419 | 
            +
                    destdir: "#{library}@#{version}",
         | 
| 420 | 
            +
                  })
         | 
| 421 | 
            +
                  return dict
         | 
| 422 | 
            +
                end
         | 
| 423 | 
            +
             | 
| 424 | 
            +
                def latest_version(library)
         | 
| 425 | 
            +
                  validate(library, nil)
         | 
| 426 | 
            +
                  json = fetch("#{API_URL}/package/npm/#{library}")
         | 
| 427 | 
            +
                  jdict = JSON.load(json)
         | 
| 428 | 
            +
                  return jdict["tags"]["latest"]
         | 
| 429 | 
            +
                end
         | 
| 430 | 
            +
             | 
| 431 | 
            +
              end
         | 
| 432 | 
            +
             | 
| 433 | 
            +
             | 
| 434 | 
            +
              class Unpkg < Base
         | 
| 435 | 
            +
                CODE = "unpkg"
         | 
| 436 | 
            +
                SITE_URL = "https://unpkg.com/"
         | 
| 437 | 
            +
                #API_URL  = "https://www.npmjs.com"
         | 
| 438 | 
            +
                API_URL  = "https://api.npms.io/v2"
         | 
| 439 | 
            +
                LIBRARY_REXP = /\A([-.\w]+|\@[-\w]+\/[-.\w]+)\z/
         | 
| 440 | 
            +
             | 
| 441 | 
            +
                protected
         | 
| 442 | 
            +
             | 
| 443 | 
            +
                def http_get(url)
         | 
| 444 | 
            +
                  return URI.__send__(:open, url, 'rb', {"x-spiferack"=>"1"}) {|f| f.read() }
         | 
| 445 | 
            +
                end
         | 
| 446 | 
            +
             | 
| 447 | 
            +
                public
         | 
| 448 | 
            +
             | 
| 449 | 
            +
                def list()
         | 
| 450 | 
            +
                  return nil    # nil means that this CDN can't list libraries without pattern
         | 
| 451 | 
            +
                end
         | 
| 452 | 
            +
             | 
| 453 | 
            +
                def search(pattern)
         | 
| 454 | 
            +
                  #json = fetch("#{API_URL}/search?q=#{pattern}")
         | 
| 455 | 
            +
                  json = fetch("#{API_URL}/search?q=#{pattern}&size=250")
         | 
| 456 | 
            +
                  jdata = JSON.load(json)
         | 
| 457 | 
            +
                  _debug_print(jdata)
         | 
| 458 | 
            +
                  #arr = jdata["objects"]   # www.npmjs.com
         | 
| 459 | 
            +
                  arr = jdata["results"]    # api.npms.io
         | 
| 460 | 
            +
                  return arr.select {|dict|
         | 
| 461 | 
            +
                    File.fnmatch(pattern, dict["package"]["name"], File::FNM_CASEFOLD)
         | 
| 462 | 
            +
                  }.collect {|dict|
         | 
| 463 | 
            +
                    d = dict["package"]
         | 
| 464 | 
            +
                    {name: d["name"], desc: d["description"], version: d["version"]}
         | 
| 465 | 
            +
                  }
         | 
| 466 | 
            +
                end
         | 
| 467 | 
            +
             | 
| 468 | 
            +
                def find(library)
         | 
| 469 | 
            +
                  validate(library, nil)
         | 
| 470 | 
            +
                  json = fetch("#{API_URL}/package/#{library.sub('/', '%2f')}", library)
         | 
| 471 | 
            +
                  jdata = JSON.load(json)
         | 
| 472 | 
            +
                  _debug_print(jdata)
         | 
| 473 | 
            +
                  dict = jdata["collected"]["metadata"]
         | 
| 474 | 
            +
                  versions = [dict["version"]]
         | 
| 475 | 
            +
                  #
         | 
| 476 | 
            +
                  url = File.join(SITE_URL, "/browse/#{library}/")
         | 
| 477 | 
            +
                  html = fetch(url, library)
         | 
| 478 | 
            +
                  _debug_print(html)
         | 
| 479 | 
            +
                  if html =~ /<script>window.__DATA__\s*=\s*(.*?)<\/script>/m
         | 
| 480 | 
            +
                    jdata2 = JSON.load($1)
         | 
| 481 | 
            +
                    versions = jdata2["availableVersions"].reverse()
         | 
| 482 | 
            +
                  end
         | 
| 483 | 
            +
                  #
         | 
| 219 484 | 
             
                  return {
         | 
| 485 | 
            +
                    name:      dict["name"],
         | 
| 486 | 
            +
                    desc:      dict["description"],
         | 
| 487 | 
            +
                    tags:      (dict["keywords"] || []).join(", "),
         | 
| 488 | 
            +
                    site:      dict["links"] ? dict["links"]["homepage"] : dict["links"]["npm"],
         | 
| 489 | 
            +
                    info:      File.join(SITE_URL, "/browse/#{library}/"),
         | 
| 490 | 
            +
                    versions:  versions,
         | 
| 491 | 
            +
                    license:   dict["license"],
         | 
| 492 | 
            +
                  }
         | 
| 493 | 
            +
                end
         | 
| 494 | 
            +
             | 
| 495 | 
            +
                def get(library, version)
         | 
| 496 | 
            +
                  validate(library, version)
         | 
| 497 | 
            +
                  dict = find(library)
         | 
| 498 | 
            +
                  dict.delete(:versions)
         | 
| 499 | 
            +
                  #
         | 
| 500 | 
            +
                  url = "#{SITE_URL}#{library}@#{version}/?meta"
         | 
| 501 | 
            +
                  begin
         | 
| 502 | 
            +
                    json = fetch(url, library)
         | 
| 503 | 
            +
                  rescue CommandError
         | 
| 504 | 
            +
                    raise CommandError.new("#{library}@#{version}: Version not found.")
         | 
| 505 | 
            +
                  end
         | 
| 506 | 
            +
                  jdata = JSON.load(json)
         | 
| 507 | 
            +
                  _debug_print(jdata)
         | 
| 508 | 
            +
                  pr = proc do |jdata, files|
         | 
| 509 | 
            +
                    jdata['files'].each do |d|
         | 
| 510 | 
            +
                      d['type'] == "directory" ? pr.call(d, files) \
         | 
| 511 | 
            +
                                               : (files << d['path'])
         | 
| 512 | 
            +
                    end if jdata['files']
         | 
| 513 | 
            +
                    files
         | 
| 514 | 
            +
                  end
         | 
| 515 | 
            +
                  files = pr.call(jdata, [])
         | 
| 516 | 
            +
                  #files = files.sort_by {|s| s.downcase }
         | 
| 517 | 
            +
                  baseurl = File.join(SITE_URL, "/#{library}@#{version}")
         | 
| 518 | 
            +
                  #
         | 
| 519 | 
            +
                  dict.update({
         | 
| 220 520 | 
             
                    name:     library,
         | 
| 221 521 | 
             
                    version:  version,
         | 
| 222 | 
            -
                     | 
| 522 | 
            +
                    info:     File.join(SITE_URL, "/browse/#{library}@#{version}/"),
         | 
| 523 | 
            +
                    npmpkg:   npmpkg_url(library, version),
         | 
| 524 | 
            +
                    urls:     files.collect {|x| baseurl+x },
         | 
| 223 525 | 
             
                    files:    files,
         | 
| 224 526 | 
             
                    baseurl:  baseurl,
         | 
| 225 | 
            -
             | 
| 527 | 
            +
                    #default:  jdata["default"],
         | 
| 528 | 
            +
                    destdir:  "#{library}@#{version}",
         | 
| 529 | 
            +
                    skipfile: /\.DS_Store\z/,  # downloading '.DS_Store' from UNPKG results in 403
         | 
| 530 | 
            +
                  })
         | 
| 531 | 
            +
                  return dict
         | 
| 532 | 
            +
                end
         | 
| 533 | 
            +
             | 
| 534 | 
            +
                def latest_version(library)
         | 
| 535 | 
            +
                  validate(library, nil)
         | 
| 536 | 
            +
                  version = nil
         | 
| 537 | 
            +
                  url = File.join(SITE_URL, "/browse/#{library}/")
         | 
| 538 | 
            +
                  uri = URI.parse(url)
         | 
| 539 | 
            +
                  HttpConnection.open(URI.parse(SITE_URL)) do |http|
         | 
| 540 | 
            +
                    resp = http.request('HEAD', "/browse/#{library}/")
         | 
| 541 | 
            +
                    if resp.code == "302"    # 302 Found
         | 
| 542 | 
            +
                      location = resp.header['Location']
         | 
| 543 | 
            +
                      location =~ /@([^@\/]+)\/\z/
         | 
| 544 | 
            +
                      version = $1
         | 
| 545 | 
            +
                    end
         | 
| 546 | 
            +
                  end
         | 
| 547 | 
            +
                  version ||= super(library)
         | 
| 548 | 
            +
                  return version
         | 
| 226 549 | 
             
                end
         | 
| 227 550 |  | 
| 228 551 | 
             
              end
         | 
| 229 552 |  | 
| 230 553 |  | 
| 231 | 
            -
              class GoogleCDN < Base | 
| 554 | 
            +
              class GoogleCDN < Base
         | 
| 232 555 | 
             
                CODE = "google"
         | 
| 233 | 
            -
                SITE_URL =  | 
| 556 | 
            +
                SITE_URL = "https://developers.google.com/speed/libraries/"
         | 
| 557 | 
            +
                CDN_URL  = "https://ajax\.googleapis\.com/ajax/libs"
         | 
| 234 558 |  | 
| 235 | 
            -
                def list
         | 
| 559 | 
            +
                def list()
         | 
| 560 | 
            +
                  html = fetch(SITE_URL)
         | 
| 561 | 
            +
                  _debug_print(html)
         | 
| 562 | 
            +
                  rexp = %r`"#{Regexp.escape(CDN_URL)}/([^/]+)/([^/]+)/([^"]+)"`
         | 
| 236 563 | 
             
                  libs = []
         | 
| 237 | 
            -
                  html = fetch("https://developers.google.com/speed/libraries/")
         | 
| 238 | 
            -
                  rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/([^/]+)/([^/]+)/([^"]+)"`
         | 
| 239 564 | 
             
                  html.scan(rexp) do |lib, ver, file|
         | 
| 240 565 | 
             
                    libs << {name: lib, desc: "latest version: #{ver}" }
         | 
| 241 566 | 
             
                  end
         | 
| @@ -244,8 +569,9 @@ module CDNGet | |
| 244 569 |  | 
| 245 570 | 
             
                def find(library)
         | 
| 246 571 | 
             
                  validate(library, nil)
         | 
| 247 | 
            -
                  html = fetch( | 
| 248 | 
            -
                   | 
| 572 | 
            +
                  html = fetch(SITE_URL)
         | 
| 573 | 
            +
                  _debug_print(html)
         | 
| 574 | 
            +
                  rexp = %r`"#{Regexp.escape(CDN_URL)}/#{library}/`
         | 
| 249 575 | 
             
                  site_url = nil
         | 
| 250 576 | 
             
                  versions = []
         | 
| 251 577 | 
             
                  urls = []
         | 
| @@ -255,9 +581,7 @@ module CDNGet | |
| 255 581 | 
             
                      found = true
         | 
| 256 582 | 
             
                      if text =~ /<dt>.*?snippet:<\/dt>\s*<dd>(.*?)<\/dd>/m
         | 
| 257 583 | 
             
                        s = $1
         | 
| 258 | 
            -
                        s.scan(/\b(?:src|href)="([^"]*?)"/)  | 
| 259 | 
            -
                          urls << href
         | 
| 260 | 
            -
                        end
         | 
| 584 | 
            +
                        s.scan(/\b(?:src|href)="([^"]*?)"/) {|href,| urls << href }
         | 
| 261 585 | 
             
                      end
         | 
| 262 586 | 
             
                      if text =~ /<dt>site:<\/dt>\s*<dd>(.*?)<\/dd>/m
         | 
| 263 587 | 
             
                        s = $1
         | 
| @@ -278,6 +602,7 @@ module CDNGet | |
| 278 602 | 
             
                  return {
         | 
| 279 603 | 
             
                    name: library,
         | 
| 280 604 | 
             
                    site: site_url,
         | 
| 605 | 
            +
                    info: "#{SITE_URL}\##{library}",
         | 
| 281 606 | 
             
                    urls: urls,
         | 
| 282 607 | 
             
                    versions: versions,
         | 
| 283 608 | 
             
                  }
         | 
| @@ -286,18 +611,19 @@ module CDNGet | |
| 286 611 | 
             
                def get(library, version)
         | 
| 287 612 | 
             
                  validate(library, version)
         | 
| 288 613 | 
             
                  d = find(library)
         | 
| 289 | 
            -
                  d[:versions].find | 
| 290 | 
            -
                    raise CommandError.new("#{version}:  | 
| 614 | 
            +
                  d[:versions].find {|s| s == version }  or
         | 
| 615 | 
            +
                    raise CommandError.new("#{library} #{version}: Version not found.")
         | 
| 291 616 | 
             
                  urls = d[:urls]
         | 
| 292 617 | 
             
                  if urls
         | 
| 293 618 | 
             
                    rexp = /(\/libs\/#{library})\/[^\/]+/
         | 
| 294 619 | 
             
                    urls = urls.collect {|x| x.gsub(rexp, "\\1/#{version}") }
         | 
| 295 620 | 
             
                  end
         | 
| 296 | 
            -
                  baseurl = " | 
| 621 | 
            +
                  baseurl = "#{CDN_URL}/#{library}/#{version}"
         | 
| 297 622 | 
             
                  files = urls ? urls.collect {|x| x[baseurl.length..-1] } : nil
         | 
| 298 623 | 
             
                  return {
         | 
| 299 624 | 
             
                    name:    d[:name],
         | 
| 300 625 | 
             
                    site:    d[:site],
         | 
| 626 | 
            +
                    info:    "#{SITE_URL}\##{library}",
         | 
| 301 627 | 
             
                    urls:    urls,
         | 
| 302 628 | 
             
                    files:   files,
         | 
| 303 629 | 
             
                    baseurl: baseurl,
         | 
| @@ -330,24 +656,34 @@ module CDNGet | |
| 330 656 | 
             
                  @script = script || File.basename($0)
         | 
| 331 657 | 
             
                end
         | 
| 332 658 |  | 
| 333 | 
            -
                def help_message
         | 
| 659 | 
            +
                def help_message()
         | 
| 334 660 | 
             
                  script = @script
         | 
| 335 661 | 
             
                  return <<END
         | 
| 336 | 
            -
            #{script}  -- download files from public CDN
         | 
| 662 | 
            +
            #{script}  -- download files from public CDN (cdnjs/jsdelivr/unpkg/google)
         | 
| 337 663 |  | 
| 338 | 
            -
            Usage: #{script} [options] [CDN | 
| 664 | 
            +
            Usage: #{script} [<options>] [<CDN> [<library> [<version> [<directory>]]]]
         | 
| 339 665 |  | 
| 340 666 | 
             
            Options:
         | 
| 341 667 | 
             
              -h, --help        : help
         | 
| 342 668 | 
             
              -v, --version     : version
         | 
| 343 669 | 
             
              -q, --quiet       : minimal output
         | 
| 670 | 
            +
                  --debug       : (debug mode)
         | 
| 344 671 |  | 
| 345 672 | 
             
            Example:
         | 
| 346 | 
            -
              $ #{script} | 
| 347 | 
            -
              $ #{script} [-q] cdnjs | 
| 348 | 
            -
              $ #{script} [-q] cdnjs jquery | 
| 349 | 
            -
              $ #{script} [-q] cdnjs jquery | 
| 350 | 
            -
              $ #{script} [-q] cdnjs jquery  | 
| 673 | 
            +
              $ #{script}                              # list public CDN names
         | 
| 674 | 
            +
              $ #{script} [-q] cdnjs                   # list libraries (except jsdelivr/unpkg)
         | 
| 675 | 
            +
              $ #{script} [-q] cdnjs 'jquery*'         # search libraries
         | 
| 676 | 
            +
              $ #{script} [-q] cdnjs jquery            # list versions
         | 
| 677 | 
            +
              $ #{script} [-q] cdnjs jquery latest     # show latest version
         | 
| 678 | 
            +
              $ #{script} [-q] cdnjs jquery 2.2.0      # list files
         | 
| 679 | 
            +
              $ mkdir -p static/lib                    # create a directory
         | 
| 680 | 
            +
              $ #{script} [-q] cdnjs jquery 2.2.0 static/lib  # download files
         | 
| 681 | 
            +
              static/lib/jquery/2.2.0/jquery.js ... Done (258,388 byte)
         | 
| 682 | 
            +
              static/lib/jquery/2.2.0/jquery.min.js ... Done (85,589 byte)
         | 
| 683 | 
            +
              static/lib/jquery/2.2.0/jquery.min.map ... Done (129,544 byte)
         | 
| 684 | 
            +
              $ ls static/lib/jquery/2.2.0
         | 
| 685 | 
            +
              jquery.js       jquery.min.js   jquery.min.map
         | 
| 686 | 
            +
             | 
| 351 687 | 
             
            END
         | 
| 352 688 | 
             
                end
         | 
| 353 689 |  | 
| @@ -362,13 +698,11 @@ END | |
| 362 698 | 
             
                end
         | 
| 363 699 |  | 
| 364 700 | 
             
                def run(*args)
         | 
| 365 | 
            -
                  cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet"])
         | 
| 701 | 
            +
                  cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet", "debug"])
         | 
| 366 702 | 
             
                  return help_message() if cmdopts['h'] || cmdopts['help']
         | 
| 367 703 | 
             
                  return RELEASE + "\n" if cmdopts['v'] || cmdopts['version']
         | 
| 368 704 | 
             
                  @quiet = cmdopts['quiet'] || cmdopts['q']
         | 
| 369 | 
            -
                   | 
| 370 | 
            -
                  validate(args[1], args[2])
         | 
| 371 | 
            -
                  #
         | 
| 705 | 
            +
                  @debug_mode = cmdopts['debug']
         | 
| 372 706 | 
             
                  case args.length
         | 
| 373 707 | 
             
                  when 0
         | 
| 374 708 | 
             
                    return do_list_cdns()
         | 
| @@ -377,11 +711,9 @@ END | |
| 377 711 | 
             
                    return do_list_libraries(cdn_code)
         | 
| 378 712 | 
             
                  when 2
         | 
| 379 713 | 
             
                    cdn_code, library = args
         | 
| 380 | 
            -
                     | 
| 381 | 
            -
             | 
| 382 | 
            -
             | 
| 383 | 
            -
                      return do_find_library(cdn_code, library)
         | 
| 384 | 
            -
                    end
         | 
| 714 | 
            +
                    return library.include?('*') \
         | 
| 715 | 
            +
                           ? do_search_libraries(cdn_code, library) \
         | 
| 716 | 
            +
                           : do_find_library(cdn_code, library)
         | 
| 385 717 | 
             
                  when 3
         | 
| 386 718 | 
             
                    cdn_code, library, version = args
         | 
| 387 719 | 
             
                    return do_get_library(cdn_code, library, version)
         | 
| @@ -394,17 +726,6 @@ END | |
| 394 726 | 
             
                  end
         | 
| 395 727 | 
             
                end
         | 
| 396 728 |  | 
| 397 | 
            -
                def validate(library, version)
         | 
| 398 | 
            -
                  if library && ! library.include?('*')
         | 
| 399 | 
            -
                    library =~ /\A[-.\w]+\z/  or
         | 
| 400 | 
            -
                      raise CommandError.new("#{library}: Unexpected library name.")
         | 
| 401 | 
            -
                  end
         | 
| 402 | 
            -
                  if version
         | 
| 403 | 
            -
                    version =~ /\A[-.\w]+\z/  or
         | 
| 404 | 
            -
                      raise CommandError.new("#{version}: Unexpected version number.")
         | 
| 405 | 
            -
                  end
         | 
| 406 | 
            -
                end
         | 
| 407 | 
            -
             | 
| 408 729 | 
             
                def parse_cmdopts(cmdargs, short_opts, long_opts)
         | 
| 409 730 | 
             
                  cmdopts = {}
         | 
| 410 731 | 
             
                  while cmdargs[0] && cmdargs[0].start_with?('-')
         | 
| @@ -435,88 +756,96 @@ END | |
| 435 756 | 
             
                def find_cdn(cdn_code)
         | 
| 436 757 | 
             
                  klass = CLASSES.find {|c| c::CODE == cdn_code }  or
         | 
| 437 758 | 
             
                    raise CommandError.new("#{cdn_code}: no such CDN.")
         | 
| 438 | 
            -
                  return klass.new
         | 
| 759 | 
            +
                  return klass.new(debug_mode: @debug_mode)
         | 
| 439 760 | 
             
                end
         | 
| 440 761 |  | 
| 441 762 | 
             
                def render_list(list)
         | 
| 442 | 
            -
                  if @quiet
         | 
| 443 | 
            -
             | 
| 444 | 
            -
                  else
         | 
| 445 | 
            -
                    return list.collect {|d| "%-20s  # %s\n" % [d[:name], d[:desc]] }.join()
         | 
| 446 | 
            -
                  end
         | 
| 763 | 
            +
                  return list.collect {|d| "#{d[:name]}\n" }.join() if @quiet
         | 
| 764 | 
            +
                  return list.collect {|d| "%-20s  # %s\n" % [d[:name], d[:desc]] }.join()
         | 
| 447 765 | 
             
                end
         | 
| 448 766 |  | 
| 449 | 
            -
                def do_list_cdns
         | 
| 450 | 
            -
                  if @quiet
         | 
| 451 | 
            -
             | 
| 452 | 
            -
                  else
         | 
| 453 | 
            -
                    return CLASSES.map {|c| "%-10s  # %s\n" % [c::CODE, c::SITE_URL] }.join()
         | 
| 454 | 
            -
                  end
         | 
| 767 | 
            +
                def do_list_cdns()
         | 
| 768 | 
            +
                  return CLASSES.map {|c| "#{c::CODE}\n" }.join() if @quiet
         | 
| 769 | 
            +
                  return CLASSES.map {|c| "%-10s  # %s\n" % [c::CODE, c::SITE_URL] }.join()
         | 
| 455 770 | 
             
                end
         | 
| 456 771 |  | 
| 457 772 | 
             
                def do_list_libraries(cdn_code)
         | 
| 458 773 | 
             
                  cdn = find_cdn(cdn_code)
         | 
| 459 | 
            -
                   | 
| 774 | 
            +
                  list = cdn.list()  or
         | 
| 775 | 
            +
                    raise CommandError.new("#{cdn_code}: cannot list libraries; please specify pattern such as 'jquery*'.")
         | 
| 776 | 
            +
                  return render_list(list)
         | 
| 460 777 | 
             
                end
         | 
| 461 778 |  | 
| 462 779 | 
             
                def do_search_libraries(cdn_code, pattern)
         | 
| 463 780 | 
             
                  cdn = find_cdn(cdn_code)
         | 
| 464 | 
            -
                   | 
| 465 | 
            -
                  rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
         | 
| 466 | 
            -
                  return render_list(cdn.list.select {|a| a[:name] =~ rexp })
         | 
| 781 | 
            +
                  return render_list(cdn.search(pattern))
         | 
| 467 782 | 
             
                end
         | 
| 468 783 |  | 
| 469 784 | 
             
                def do_find_library(cdn_code, library)
         | 
| 470 785 | 
             
                  cdn = find_cdn(cdn_code)
         | 
| 471 786 | 
             
                  d = cdn.find(library)
         | 
| 472 | 
            -
                   | 
| 787 | 
            +
                  buf = []
         | 
| 473 788 | 
             
                  if @quiet
         | 
| 474 789 | 
             
                    d[:versions].each do |ver|
         | 
| 475 | 
            -
                       | 
| 476 | 
            -
                    end  | 
| 790 | 
            +
                      buf << "#{ver}\n"
         | 
| 791 | 
            +
                    end unless empty?(d[:versions])
         | 
| 477 792 | 
             
                  else
         | 
| 478 | 
            -
                     | 
| 479 | 
            -
                     | 
| 480 | 
            -
                     | 
| 481 | 
            -
                     | 
| 482 | 
            -
                     | 
| 483 | 
            -
                     | 
| 793 | 
            +
                    buf << "name:     #{d[:name]}\n"
         | 
| 794 | 
            +
                    buf << "desc:     #{d[:desc]}\n"    unless empty?(d[:desc])
         | 
| 795 | 
            +
                    buf << "tags:     #{d[:tags]}\n"    unless empty?(d[:tags])
         | 
| 796 | 
            +
                    buf << "site:     #{d[:site]}\n"    unless empty?(d[:site])
         | 
| 797 | 
            +
                    buf << "info:     #{d[:info]}\n"    unless empty?(d[:info])
         | 
| 798 | 
            +
                    buf << "license:  #{d[:license]}\n" unless empty?(d[:license])
         | 
| 799 | 
            +
                    buf << "snippet: |\n" << d[:snippet].gsub(/^/, '    ') unless empty?(d[:snippet])
         | 
| 800 | 
            +
                    buf << "versions:\n"
         | 
| 484 801 | 
             
                    d[:versions].each do |ver|
         | 
| 485 | 
            -
                       | 
| 486 | 
            -
                    end  | 
| 802 | 
            +
                      buf << "  - #{ver}\n"
         | 
| 803 | 
            +
                    end unless empty?(d[:versions])
         | 
| 487 804 | 
             
                  end
         | 
| 488 | 
            -
                  return  | 
| 805 | 
            +
                  return buf.join()
         | 
| 489 806 | 
             
                end
         | 
| 490 807 |  | 
| 491 808 | 
             
                def do_get_library(cdn_code, library, version)
         | 
| 492 809 | 
             
                  cdn = find_cdn(cdn_code)
         | 
| 810 | 
            +
                  version = cdn.latest_version(library) if version == 'latest'
         | 
| 493 811 | 
             
                  d = cdn.get(library, version)
         | 
| 494 | 
            -
                   | 
| 812 | 
            +
                  buf = []
         | 
| 495 813 | 
             
                  if @quiet
         | 
| 496 814 | 
             
                    d[:urls].each do |url|
         | 
| 497 | 
            -
                       | 
| 815 | 
            +
                      buf << "#{url}\n"
         | 
| 498 816 | 
             
                    end if d[:urls]
         | 
| 499 817 | 
             
                  else
         | 
| 500 | 
            -
                     | 
| 501 | 
            -
                     | 
| 502 | 
            -
                     | 
| 503 | 
            -
                     | 
| 504 | 
            -
                     | 
| 505 | 
            -
                     | 
| 506 | 
            -
                     | 
| 818 | 
            +
                    buf << "name:     #{d[:name]}\n"
         | 
| 819 | 
            +
                    buf << "version:  #{d[:version]}\n"
         | 
| 820 | 
            +
                    buf << "desc:     #{d[:desc]}\n"    unless empty?(d[:desc])
         | 
| 821 | 
            +
                    buf << "tags:     #{d[:tags]}\n"    unless empty?(d[:tags])
         | 
| 822 | 
            +
                    buf << "site:     #{d[:site]}\n"    unless empty?(d[:site])
         | 
| 823 | 
            +
                    buf << "info:     #{d[:info]}\n"    unless empty?(d[:info])
         | 
| 824 | 
            +
                    buf << "npmpkg:   #{d[:npmpkg]}\n"  unless empty?(d[:npmpkg])
         | 
| 825 | 
            +
                    buf << "default:  #{d[:default]}\n" unless empty?(d[:default])
         | 
| 826 | 
            +
                    buf << "license:  #{d[:license]}\n" unless empty?(d[:license])
         | 
| 827 | 
            +
                    buf << "snippet: |\n" << d[:snippet].gsub(/^/, '    ') unless empty?(d[:snippet])
         | 
| 828 | 
            +
                    buf << "urls:\n" unless empty?(d[:urls])
         | 
| 507 829 | 
             
                    d[:urls].each do |url|
         | 
| 508 | 
            -
                       | 
| 509 | 
            -
                    end  | 
| 830 | 
            +
                      buf << "  - #{url}\n"
         | 
| 831 | 
            +
                    end unless empty?(d[:urls])
         | 
| 510 832 | 
             
                  end
         | 
| 511 | 
            -
                  return  | 
| 833 | 
            +
                  return buf.join()
         | 
| 512 834 | 
             
                end
         | 
| 513 835 |  | 
| 514 836 | 
             
                def do_download_library(cdn_code, library, version, basedir)
         | 
| 515 837 | 
             
                  cdn = find_cdn(cdn_code)
         | 
| 838 | 
            +
                  version = cdn.latest_version(library) if version == 'latest'
         | 
| 516 839 | 
             
                  cdn.download(library, version, basedir, quiet: @quiet)
         | 
| 517 840 | 
             
                  return nil
         | 
| 518 841 | 
             
                end
         | 
| 519 842 |  | 
| 843 | 
            +
                private
         | 
| 844 | 
            +
             | 
| 845 | 
            +
                def empty?(x)
         | 
| 846 | 
            +
                  return x.nil? || x.empty?
         | 
| 847 | 
            +
                end
         | 
| 848 | 
            +
             | 
| 520 849 | 
             
              end
         | 
| 521 850 |  | 
| 522 851 |  |