├── .gitignore ├── .travis.yml ├── .yardopts ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── torba ├── lib ├── torba.rb └── torba │ ├── cli.rb │ ├── css_url_to_erb_asset_path.rb │ ├── import_list.rb │ ├── manifest.rb │ ├── package.rb │ ├── rake_task.rb │ ├── remote_sources │ ├── common.rb │ ├── get_file.rb │ ├── github_release.rb │ ├── npm.rb │ ├── targz.rb │ └── zip.rb │ ├── ui.rb │ └── verify.rb ├── test ├── acceptance-cli │ ├── open_test.rb │ ├── pack_test.rb │ ├── show_test.rb │ └── verify_test.rb ├── css_url_to_erb_asset_path_test.rb ├── fixtures │ ├── home_path │ │ ├── 01 │ │ │ └── trumbowyg │ │ │ │ ├── icons-2x.png │ │ │ │ ├── icons.png │ │ │ │ ├── trumbowyg.css.erb │ │ │ │ └── trumbowyg.js │ │ ├── 02 │ │ │ └── lo_dash │ │ │ │ └── lodash.js │ │ └── 03 │ │ │ └── bourbon │ │ │ ├── _border-image.scss │ │ │ ├── _font-source-declaration.scss │ │ │ └── _retina-image.scss │ └── torbafiles │ │ ├── 01_gh_release.rb │ │ ├── 01_image_asset_not_specified.rb │ │ ├── 01_targz.rb │ │ ├── 01_zip.rb │ │ ├── 02_npm.rb │ │ ├── 03_not_existed_assets.rb │ │ └── 04_similar_names.rb ├── import_list_test.rb ├── manifest_test.rb ├── package │ ├── import_list_test.rb │ └── logical_paths_test.rb ├── package_test.rb ├── rake_task_test.rb ├── remote_sources │ ├── common_test.rb │ ├── get_file_test.rb │ ├── github_release_test.rb │ ├── npm_test.rb │ ├── targz_test.rb │ └── zip_test.rb ├── test_helper.rb └── torba_test.rb └── torba.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1 6 | - 2.2 7 | - 2.3 8 | - 2.4 9 | - 2.5 10 | - 2.6 11 | - 2.7 12 | - jruby 13 | bundler_args: --without doc debug 14 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | - 3 | CHANGELOG.md 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ## Version 1.2.0 4 | 5 | ### Enhancements 6 | 7 | * Support scoped names for NPM packages (e.g. "@lottiefiles/lottie-player") 8 | 9 | ## Version 1.1.1 10 | 11 | ### Enhancements 12 | 13 | * Test files have been removed from the package to reduce 14 | file size (and to spend less internet traffic too). 15 | 16 | ## Version 1.1.0 17 | 18 | ### Enhancements 19 | 20 | * Thor dependency has been relaxed to support Rails 6.x. 21 | 22 | ## Version 1.0.1 23 | 24 | ### Bug fixes 25 | 26 | * Fix unpacking tar.gz when running on unpriveleged containers 27 | 28 | ## Version 1.0.0 29 | 30 | ### Breaking changes 31 | 32 | * Rails support has been removed. Use [torba-rails][torba-rails] instead. 33 | * `torba/verify` no longer checks, whether it is executed within Rake task 34 | or not. This was a workaround for Rails support, which is no longer a goal 35 | of this gem. 36 | * `torba/verify` no longer checks `TORBA_DONT_VERIFY` env variable. Same as 37 | above. 38 | 39 | ### Upgrading notes 40 | 41 | If you are a non-Rails user, this should be a non-breaking update. In rare cases, 42 | when you depend on old behaviour of `torba/verify`, please, add the conditionals 43 | to your application code. 44 | 45 | Rails users should: 46 | 47 | 1. Replace `gem "torba"` with `gem "torba-rails"` in the Gemfile. 48 | 2. Remove `require "torba/verify"` from `boot.rb`. 49 | 3. Remove `require "torba/rails"` from `application.rb`. 50 | 4. Unset obsolete `TORBA_DONT_VERIFY` env variable if present. 51 | 52 | ### Enhancements 53 | 54 | * Rake task accepts a block to be executed before packing process 55 | 56 | [torba-rails]: https://github.com/torba-rb/torba-rails 57 | 58 | ## Version 0.7.0 59 | 60 | ### Enhancements 61 | 62 | * `torba show` command 63 | * `torba open` command 64 | 65 | ## Version 0.6.0 66 | 67 | ### Enhancements 68 | 69 | * Torba.cache_path= setter 70 | 71 | ### Bug fixes 72 | 73 | * Handle stylesheets that mention nonexisting assets (e.g. with SCSS variables) 74 | 75 | ## Version 0.5.1 76 | 77 | ### Bug fixes 78 | 79 | * Treat SASS/SCSS files as regular stylesheets 80 | 81 | ## Version 0.5.0 82 | 83 | ### Upgrading notes 84 | 85 | Rails users are advised to remove manual execution of `torba pack` from 86 | their deployment scripts, since the command now is a part of 87 | `rake assets:precompile`. 88 | 89 | ### Enhancements 90 | 91 | * Support deployment to Heroku 92 | * `torba pack` is automatically injected into `rake assets:precompile` 93 | in Rails applications 94 | * `torba install` command is a mapping for `torba pack` 95 | 96 | ### Bug fixes 97 | 98 | * Retry 5 times if fetching a remote file fails 99 | 100 | ## Version 0.4.2 101 | 102 | ### Bug fixes 103 | 104 | * Remote url() assets should not be given to asset_path 105 | * Support both url(data: and url('data: in css 106 | 107 | ## Version 0.4.1 108 | 109 | ### Bug fixes 110 | 111 | * Do not add the `.erb` extension for stylesheets that don't need it. 112 | 113 | ## Version 0.4.0 114 | 115 | ### Enhancements 116 | 117 | * The exception that gets raised during `torba verify` now list the missing 118 | packages 119 | 120 | ## Version 0.3.1 121 | 122 | ### Bug fixes 123 | 124 | * Fix no asset found when a CSS url contains "?#iefix" or "#svg-fragment" 125 | 126 | ## Version 0.3.0 127 | 128 | ### Enhancements 129 | 130 | * Rails setup automatically populates `Rails.application.config.assets.precompile` 131 | If libraries added via Torba have image/font content, you can remove it from 132 | that list, no need for manual manipulation 133 | * Support .tar.gz remote sources 134 | * Support npm packages 135 | 136 | ## Version 0.2.1 137 | 138 | ### Bug fixes 139 | 140 | * Cached GithubRelease remote contains in path repository name, not nameless 141 | version 142 | 143 | ## Version 0.2.0 144 | 145 | ### Enhancements 146 | 147 | * Name for GH releases can be optional 148 | * Cached zip remote contains URL filename in path for better introspection 149 | * GithubRelease remote: introduce #repository_user, #repository_name 150 | 151 | ### Bug fixes 152 | 153 | * Display actual exception (if any) instead of SystemExitError for pow 154 | * Remote source always returns absolute path even if Torba.home_path/cache_path 155 | is relative to current directory 156 | 157 | ## Version 0.1.1 158 | 159 | ### Bug fixes 160 | 161 | * Fail fast and report on 404 resources 162 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Adding a feature 4 | 5 | 1. Open an issue and explain what you're planning to do. It is better to discuss new idea first, 6 | rather when diving into code. 7 | 2. Add some tests. 8 | 3. Write the code. 9 | 4. Make sure all tests pass. 10 | 5. Commit with detailed explanation what you've done in a message. 11 | 6. Open pull request. 12 | 13 | ## Breaking/removing a feature 14 | 15 | 1. Add deprecation warning and fallback to old behaivour if possible. 16 | 2. Explain how to migrate to the new code in CHANGELOG. 17 | 3. Update/remove tests. 18 | 4. Update the code. 19 | 5. Make sure all tests pass. 20 | 6. Commit with detailed explanation what you've done in a message. 21 | 7. Open pull request. 22 | 23 | ## Fixing a bug 24 | 25 | 1. Add failing test. 26 | 2. Fix the bug. 27 | 3. Make sure all tests pass. 28 | 4. Commit with detailed explanation what you've done in a message. 29 | 5. Open pull request. 30 | 31 | ## Fixing a typo 32 | 33 | 1. Commit with a message that include "[ci skip]" remark. 34 | 2. Open pull request. 35 | 36 | ## Running the tests 37 | 38 | ``` 39 | rake test 40 | ``` 41 | 42 | ## Working with documentation 43 | 44 | ``` 45 | yard server -dr 46 | open http://localhost:8808 47 | ``` 48 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem "pry", :group => :debug 6 | gem "yard", "~> 0.8", :group => :doc 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Andrii Malyshko 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Torba 2 | 3 | [![Build Status](https://img.shields.io/travis/torba-rb/torba.svg)](https://travis-ci.org/torba-rb/torba) 4 | [![Gem version](https://img.shields.io/gem/v/torba.svg)](https://rubygems.org/gems/torba) 5 | 6 | **Torba** is a [Bower][bower]-less asset manager for [Sprockets][sprockets]. It makes a local copy 7 | of a JS/CSS library and puts it under Sprockets' [load path][sprockets-load-path]. 8 | 9 | ## Name origin 10 | 11 | "Торба" [[tǒːrba][torba-pronounce]] in Ukrainian and "torba" in Polish, Turkic languages can mean 12 | "duffel bag", "gunny sack" or, more generally, any flexible container. 13 | 14 | ## Status 15 | 16 | Production ready. 17 | 18 | ## Documentation 19 | 20 | [Released version](http://rubydoc.info/gems/torba/1.2.0) 21 | 22 | ## Why 23 | 24 | De facto approach, i.e. wrapping JS and CSS libraries in a gem, requires a 25 | maintainer to constantly track changes in an upstream repository. Even more so, if a gem 26 | maintainer stops using that specific library, the gem will eventually become abandoned. 27 | Additionally, many libraries still have no gem wrappers. 28 | 29 | Other alternatives: 30 | 31 | * [rails-assets][rails-assets] relies on Bower *and* it is quite complex, 32 | * [bower-rails][bower-rails] relies on Bower, see below for why this can be an issue. 33 | 34 | Problems with the Bower: 35 | 36 | * it is not a part of the Ruby ecosystem, 37 | * frontend JS libraries are usually standalone (except for a potential jQuery dependency), so there's 38 | no need for a complex Bundler-like solution with tree-dependency resolution, 39 | * often we can't use optimistic version constraints, because the JavaScript community does not consistenly apply the principles of [Semver][semver]. By specifying strict versions we use Bower as a complex facade for functionality that could be accomplished with curl. 40 | 41 | ## External dependencies 42 | 43 | * curl 44 | * unzip 45 | * gzip 46 | * tar 47 | 48 | ## Design limitations 49 | 50 | * Torba doesn't do any version dependency resolution, it's up to you to specify the correct version of 51 | each asset package, 52 | * Torba doesn't do any builds, you should use remote sources with pre-built assets. 53 | 54 | ## Installation 55 | 56 | ### Rails 57 | 58 | Use [torba-rails][torba-rails-github]. 59 | 60 | ### Sinatra 61 | 62 | See this [example project][sinatra-example]. 63 | 64 | ### Other Ruby application 65 | 66 | Add this line to your application's Gemfile and run bundle: 67 | 68 | ```ruby 69 | gem 'torba' 70 | ``` 71 | 72 | ## Usage 73 | 74 | 1. Create Torbafile at the project root and commit it. 75 | 76 | 2. Run `bundle exec torba pack`. 77 | 78 | 3. Add "require" [Sprockets directives][sprockets-directives] to your "application.js" 79 | and/or "@import" [Sass directives][sass-import] to "application.css". 80 | 81 | If any changes made to the Torbafile, run `bundle exec torba pack` again. 82 | 83 | ### Torbafile 84 | 85 | Torbafile is an assets specification. It is a plain text file that contains one or more 86 | sections, each of them describes one remote source of assets. 87 | 88 | Currently only zip, tar.gz archives, [Github releases][github-releases] and 89 | [npm packages][npm] are supported. 90 | 91 | #### Zip archive package 92 | 93 | Allows to download and unpack asset package from any source accessible by curl. 94 | 95 | The syntax is: 96 | 97 | ```ruby 98 | zip "name", url: "..." [, import: %w(...)] 99 | ``` 100 | 101 | where "name" is an arbitrary name for the package, more on "import" below. For example, 102 | 103 | ```ruby 104 | zip "scroll_magic", url: "https://github.com/janpaepke/ScrollMagic/archive/v2.0.0.zip" 105 | ``` 106 | 107 | #### Tar.gz archive package 108 | 109 | The syntax is same as for a zip package: 110 | 111 | ```ruby 112 | targz "name", url: "..." [, import: %w(...)] 113 | ``` 114 | 115 | for example, 116 | 117 | ```ruby 118 | targz "scroll_magic", url: "https://github.com/janpaepke/ScrollMagic/archive/v2.0.0.tar.gz" 119 | ``` 120 | 121 | #### Github release package 122 | 123 | This is a more readable version/shortcut for "https://github.com/.../archive/..." URLs. 124 | 125 | The syntax is: 126 | 127 | ```ruby 128 | gh_release "name", source: "...", tag: "..." [, import: %w(...)] 129 | ``` 130 | 131 | where "source" is the user + repository and "tag" is the repository tag (exactly as on Github, 132 | i.e. with "v" prefix if present), more on "import" below. For example, 133 | 134 | ```ruby 135 | gh_release "scroll_magic", source: "janpaepke/ScrollMagic", tag: "v.2.0.0" 136 | ``` 137 | 138 | You can omit the name, it will be equal to the repository name: 139 | 140 | ```ruby 141 | gh_release source: "janpaepke/ScrollMagic", tag: "v.2.0.0" # "ScrollMagic" is assumed 142 | ``` 143 | 144 | #### npm package 145 | 146 | Allows to download packages from npm registry. 147 | 148 | The syntax is: 149 | 150 | ```ruby 151 | npm "name", package: "...", version: "..." [, import: %w(...)] 152 | ``` 153 | 154 | where "package" is the package name as published on npm registry and "version" is its version, 155 | more on "import" below. For example, 156 | 157 | ```ruby 158 | npm "coffee", package: "coffee-script", version: "1.9.2" 159 | ``` 160 | 161 | You can omit the name, it will be equal to the package name: 162 | 163 | ```ruby 164 | npm package: "coffee-script", version: "1.9.2" 165 | ``` 166 | 167 | ### Examples 168 | 169 | See [Torbafiles][torbafile-examples] used for testing. 170 | 171 | ### "Packing the torba" process 172 | 173 | When you run `torba pack` the following happens: 174 | 175 | 1. All remote sources are cached locally. 176 | 177 | 2. Archives are unpacked with top level directory removed. This is done for good because it 178 | usually contains the package version in the name, e.g. "react-0.13.2", and you don't want to have to reference versions 179 | inside your application code (except Torbafile). 180 | 181 | 3. Remote source's content is copied as is to the `Torba.home_path` location with **package name used 182 | as a namespace**. 183 | 184 | This is also done for good reason in order to avoid name collisions (since many JS projects can have 185 | assets with the same names and all packages are placed into Sprockets' shared virtual filesystem). 186 | The downside is that you have to use namespaces in each require directive, which can lead to 187 | duplication: 188 | 189 | ```javascript 190 | // application.js 191 | //= require 'underscore/underscore' 192 | ``` 193 | 194 | Hint: use "require_directory" if you're strongly against such duplication: 195 | 196 | ```javascript 197 | //= require_directory 'underscore' 198 | ``` 199 | 200 | 4. Stylesheets (if any) are converted to ".css.erb" with "asset_path" helpers used in "url(...)" 201 | statements. 202 | 203 | ### :import option 204 | 205 | Copying whole remote source's content has the disadvantage of using remote source specific paths in your 206 | require/import directives. For example, if an archive contains files in the "dist/css" directory, you'll have 207 | to mention it: 208 | 209 | ```css 210 | /* application.css */ 211 | @import 'lightslider/dist/css/lightslider'; 212 | ``` 213 | 214 | To mitigate this you can cherry-pick files from the source via the "import" option, for example: 215 | 216 | ```ruby 217 | gh_release "lightslider", source: "sachinchoolur/lightslider", tag: "1.1.2", import: %w[ 218 | dist/css/lightslider.css 219 | ] 220 | ``` 221 | 222 | Such files will be copied directly to the package root (i.e. file tree becomes flatten), thus you 223 | can omit unnecessary paths: 224 | 225 | ```css 226 | @import 'lightslider/lightslider'; 227 | ``` 228 | 229 | You can use any Dir.glob pattern: 230 | 231 | ```ruby 232 | gh_release "lightslider", source: "sachinchoolur/lightslider", tag: "1.1.2", import: %w[ 233 | dist/css/lightslider.css 234 | dist/img/*.png 235 | ] 236 | ``` 237 | 238 | In addition to this "path/" is treated as a shortcut for "path/**/*" glob pattern. 239 | 240 | [bower]: http://bower.io/ 241 | [sprockets]: https://github.com/rails/sprockets/ 242 | [sprockets-load-path]: https://github.com/rails/sprockets#the-load-path 243 | [torba-pronounce]: http://upload.wikimedia.org/wikipedia/commons/2/28/Uk-%D1%82%D0%BE%D1%80%D0%B1%D0%B0.ogg 244 | [github-releases]: https://help.github.com/articles/about-releases/ 245 | [sprockets-directives]: https://github.com/rails/sprockets#the-directive-processor 246 | [sass-import]: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#import 247 | [rails-assets]: https://rails-assets.org/ 248 | [bower-rails]: https://github.com/rharriso/bower-rails 249 | [semver]: http://semver.org/ 250 | [npm]: https://npmjs.com 251 | [torba-rails-github]: https://github.com/torba-rb/torba-rails 252 | [sinatra-example]: https://github.com/xfalcox/sinatra-assets-seed 253 | [torbafile-examples]: https://github.com/torba-rb/torba/tree/master/test/fixtures/torbafiles 254 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | task :default => :test 4 | task :test do 5 | $LOAD_PATH.unshift("test") 6 | Dir.glob("./test/**/*_test.rb").each { |file| require file} 7 | end 8 | -------------------------------------------------------------------------------- /bin/torba: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "torba/cli" 4 | 5 | Torba::Cli.start 6 | -------------------------------------------------------------------------------- /lib/torba.rb: -------------------------------------------------------------------------------- 1 | require "digest" 2 | 3 | require "torba/ui" 4 | require "torba/manifest" 5 | 6 | # @since 0.1.0 7 | module Torba 8 | module Errors 9 | ShellCommandFailed = Class.new(StandardError) 10 | end 11 | 12 | # @return [String] root path to prepared asset packages. By default it's ".torba" within 13 | # your OS home directory (i.e. packages are shared between projects). 14 | # @note use "TORBA_HOME_PATH" env variable to override default value 15 | def self.home_path 16 | @home_path ||= ENV["TORBA_HOME_PATH"] || File.join(Dir.home, ".torba") 17 | end 18 | 19 | # Override home path with a new value 20 | # @param val [String] new home path 21 | # @return [void] 22 | def self.home_path=(val) 23 | @home_path = val 24 | end 25 | 26 | # @return [String] root path to downloaded yet unprocessed asset packages. By default 27 | # it's "cache" within {.home_path}. 28 | # @note use "TORBA_CACHE_PATH" env variable to override default value 29 | def self.cache_path 30 | @cache_path ||= ENV["TORBA_CACHE_PATH"] || File.join(home_path, "cache") 31 | end 32 | 33 | # Override cache path with a new value 34 | # @param val [String] new cache path 35 | # @return [void] 36 | # @since 0.6.0 37 | def self.cache_path=(val) 38 | @cache_path = val 39 | end 40 | 41 | # @return [Ui] 42 | def self.ui 43 | @ui ||= Ui.new 44 | end 45 | 46 | # @return [Manifest] 47 | def self.manifest 48 | @manifest ||= Manifest.build 49 | end 50 | 51 | # @see Manifest#pack 52 | def self.pack 53 | manifest.pack 54 | end 55 | 56 | # @see Manifest#load_path 57 | def self.load_path 58 | manifest.load_path 59 | end 60 | 61 | # @see Manifest#non_js_css_logical_paths 62 | # @since 0.3.0 63 | def self.non_js_css_logical_paths 64 | manifest.non_js_css_logical_paths 65 | end 66 | 67 | # @see Manifest#verify 68 | def self.verify 69 | manifest.verify 70 | end 71 | 72 | # @return [String] unique short fingerprint/hash for given string 73 | # @param [String] string to be hashed 74 | # 75 | # @example 76 | # Torba.digest("path/to/hash") #=> "23e3e63c" 77 | def self.digest(string) 78 | Digest::SHA1.hexdigest(string)[0..7] 79 | end 80 | 81 | # @see Manifest#find_packages_by_name 82 | # @since 0.7.0 83 | def self.find_packages_by_name(name) 84 | manifest.find_packages_by_name(name) 85 | end 86 | 87 | # @yield a block, converts common exceptions into useful messages 88 | def self.pretty_errors 89 | yield 90 | rescue Errors::MissingPackages => e 91 | ui.error "Your Torba is not packed yet." 92 | ui.error "Missing packages:" 93 | e.packages.each do |package| 94 | ui.error " * #{package.name}" 95 | end 96 | ui.suggest "Run `bundle exec torba pack` to install missing packages." 97 | exit(false) 98 | rescue Errors::ShellCommandFailed => e 99 | ui.error "Couldn't execute command '#{e.message}'" 100 | exit(false) 101 | rescue Errors::NothingToImport => e 102 | ui.error "Couldn't import an asset(-s) '#{e.path}' from import list in '#{e.package}'." 103 | ui.suggest "Check for typos." 104 | ui.suggest "Make sure that the path has trailing '/' if its a directory." 105 | exit(false) 106 | rescue Errors::AssetNotFound => e 107 | ui.error "Unknown asset to process with path '#{e.message}'." 108 | ui.suggest "Make sure that you've imported all image/font assets mentioned in a stylesheet(-s)." 109 | exit(false) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/torba/cli.rb: -------------------------------------------------------------------------------- 1 | require "thor" 2 | require "shellwords" 3 | require "torba" 4 | 5 | module Torba 6 | class Cli < Thor 7 | desc "pack", "download and prepare all packages defined in Torbafile" 8 | def pack 9 | Torba.pretty_errors { Torba.pack } 10 | Torba.ui.confirm "Torba has been packed!" 11 | end 12 | map install: :pack 13 | 14 | desc "verify", "check if all packages are prepared" 15 | def verify 16 | Torba.pretty_errors { Torba.verify } 17 | Torba.ui.confirm "Torba is prepared!" 18 | end 19 | 20 | desc "show PACKAGE", "show the source location of a particular package" 21 | def show(name) 22 | Torba.pretty_errors do 23 | Torba.pack 24 | Torba.ui.info(find_package(name).load_path) 25 | end 26 | end 27 | 28 | desc "open PACKAGE", "open a particular package in editor" 29 | def open(name) 30 | editor = [ENV["TORBA_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find { |e| !e.nil? && !e.empty? } 31 | unless editor 32 | Torba.ui.error("To open a package, set $EDITOR or $TORBA_EDITOR") 33 | exit(false) 34 | end 35 | 36 | Torba.pretty_errors do 37 | Torba.pack 38 | 39 | command = Shellwords.split(editor) << find_package(name).load_path 40 | system(*command) || Torba.ui.error("Could not run '#{command.join(" ")}'") 41 | end 42 | end 43 | 44 | private 45 | 46 | def find_package(name) 47 | packages = Torba.find_packages_by_name(name) 48 | case packages.size 49 | when 0 50 | Torba.ui.error "Could not find package '#{name}'." 51 | exit(false) 52 | when 1 53 | packages.first 54 | else 55 | index = Torba.ui.choose_one(packages.map(&:name)) 56 | if index 57 | packages[index] 58 | else 59 | exit(false) 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/torba/css_url_to_erb_asset_path.rb: -------------------------------------------------------------------------------- 1 | module Torba 2 | # Parses content of CSS file and converts its image assets paths into Sprockets' 3 | # {https://github.com/rails/sprockets#logical-paths logical paths}. 4 | class CssUrlToErbAssetPath 5 | URL_RE = 6 | / 7 | ( # $1 8 | url\( # url( 9 | \s* # optional space 10 | ['"]? # optional quote 11 | (?!data) # no data URIs 12 | (?!http[s]?:\/\/) # no remote URIs 13 | (?!\/) # only relative location 14 | ) 15 | ( # $2 16 | [^'"?#]+? # location 17 | ) 18 | ( # $3 19 | ([?#][^'"]+?)? # optional query or fragment 20 | ['"]? # optional quote 21 | \s* # optional space 22 | \) # ) 23 | ) 24 | /xm 25 | 26 | # @return [String] CSS file content where image "url(...)" paths are replaced by ERB 27 | # interpolations "url(<%= asset_path(...) %>)". 28 | # @param content [String] content of original CSS file 29 | # @param file_path [String] absolute path to original CSS file 30 | # @yield [image_file_path] 31 | # @yieldparam image_file_path [String] absolute path to original image file which is mentioned 32 | # within CSS file 33 | # @yieldreturn [String] logical path to image file within Sprockets' virtual filesystem. 34 | # 35 | # @example 36 | # content = \ 37 | # ".react-toolbar { 38 | # width: 100%; 39 | # background: url('./images/toolbar.png'); 40 | # }" 41 | # 42 | # new_content = CssUrlToErbAssetPath.call(content, "/var/downloads/react_unzipped/styles.css") do |url| 43 | # url.sub("/var/downloads/react_unzipped/images", "react-toolbar-js" 44 | # end 45 | # 46 | # new_content #=> 47 | # ".react-toolbar { 48 | # width: 100%; 49 | # background: url('<%= asset_path('react-toolbar-js/toolbar.png') %>'); 50 | # }" 51 | def self.call(content, file_path) 52 | content.gsub(URL_RE) do 53 | absolute_image_file_path = File.expand_path($2, File.dirname(file_path)) 54 | sprockets_file_path = yield absolute_image_file_path 55 | if sprockets_file_path 56 | "#{$1}<%= asset_path('#{sprockets_file_path}') %>#{$3}" 57 | else 58 | $& 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/torba/import_list.rb: -------------------------------------------------------------------------------- 1 | module Torba 2 | module Errors 3 | AssetNotFound = Class.new(StandardError) 4 | end 5 | 6 | # Represents a list of assets to be imported from a remote source. 7 | class ImportList 8 | class Asset < Struct.new(:absolute_path, :logical_path) 9 | def css? 10 | absolute_path =~ /\.(sass|scss|css)$/ 11 | end 12 | 13 | def js? 14 | absolute_path.end_with?(".js") 15 | end 16 | end 17 | 18 | # @return [Array] full list of assets to be imported. 19 | attr_reader :assets 20 | 21 | def initialize(assets) 22 | @assets = assets 23 | end 24 | 25 | # @return [Asset] asset with given path. 26 | # @param path [String] absolute path of an asset. 27 | # @raise [Errors::AssetNotFound] if nothing found 28 | def find_by_absolute_path(path) 29 | assets.find { |asset| asset.absolute_path == path } || raise(Errors::AssetNotFound.new(path)) 30 | end 31 | 32 | # @return [Array] list of stylesheets to be imported. 33 | def css_assets 34 | assets.find_all { |asset| asset.css? } 35 | end 36 | 37 | # @return [Array] list of assets to be imported except stylesheets. 38 | def non_css_assets 39 | assets.find_all { |asset| !asset.css? } 40 | end 41 | 42 | # @return [Array] list of assets to be imported except javascripts and 43 | # stylesheets. 44 | # @since 0.3.0 45 | def non_js_css_assets 46 | assets.find_all { |asset| !(asset.js? || asset.css?) } 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/torba/manifest.rb: -------------------------------------------------------------------------------- 1 | require "torba/package" 2 | require "torba/remote_sources/zip" 3 | require "torba/remote_sources/github_release" 4 | require "torba/remote_sources/targz" 5 | require "torba/remote_sources/npm" 6 | 7 | module Torba 8 | module Errors 9 | class MissingPackages < StandardError 10 | attr_reader :packages 11 | 12 | def initialize(packages) 13 | @packages = packages 14 | super 15 | end 16 | end 17 | end 18 | 19 | # Represents Torbafile. 20 | class Manifest 21 | # all packages defined in Torbafile 22 | attr_reader :packages 23 | 24 | # Reads Torbafile and evaluates it. 25 | # @return [Manifest] 26 | # 27 | # @overload self.build(file_path) 28 | # @param file_path [String] absolute path to Torbafile 29 | # 30 | # @overload self.build 31 | # Reads Torbafile from current directory 32 | def self.build(file_path = ENV["TORBA_FILE"]) 33 | file_path ||= File.join(Dir.pwd, "Torbafile") 34 | 35 | manifest = new 36 | content = File.read(file_path) 37 | manifest.instance_eval(content, file_path) 38 | manifest 39 | end 40 | 41 | def initialize 42 | @packages = [] 43 | end 44 | 45 | # Adds {Package} with {RemoteSources::Zip} to {#packages} 46 | def zip(name, options = {}) 47 | url = options.fetch(:url) 48 | remote_source = RemoteSources::Zip.new(url) 49 | packages << Package.new(name, remote_source, options) 50 | end 51 | 52 | # Adds {Package} with {RemoteSources::GithubRelease} to {#packages} 53 | def gh_release(name = nil, options = {}) 54 | if name.is_a?(Hash) 55 | options, name = name, nil 56 | end 57 | 58 | source = options.fetch(:source) 59 | tag = options.fetch(:tag) 60 | remote_source = RemoteSources::GithubRelease.new(source, tag) 61 | 62 | name ||= remote_source.repository_name 63 | packages << Package.new(name, remote_source, options) 64 | end 65 | 66 | # Adds {Package} with {RemoteSources::Targz} to {#packages} 67 | # @since 0.3.0 68 | def targz(name, options = {}) 69 | url = options.fetch(:url) 70 | remote_source = RemoteSources::Targz.new(url) 71 | packages << Package.new(name, remote_source, options) 72 | end 73 | 74 | # Adds {Package} with {RemoteSources::Npm} to {#packages} 75 | # @since 0.3.0 76 | def npm(name = nil, options = {}) 77 | if name.is_a?(Hash) 78 | options, name = name, nil 79 | end 80 | 81 | package_name = options.fetch(:package) 82 | version = options.fetch(:version) 83 | remote_source = RemoteSources::Npm.new(package_name, version) 84 | 85 | name ||= remote_source.package 86 | packages << Package.new(name, remote_source, options) 87 | end 88 | 89 | # Builds all {#packages} 90 | # @return [void] 91 | def pack 92 | packages.each(&:build) 93 | end 94 | 95 | # @return [Array] list of paths to each prepared asset package. 96 | # It should be appended to the Sprockets' load_path. 97 | def load_path 98 | packages.map(&:load_path) 99 | end 100 | 101 | # @return [Array] logical paths that packages contain except JS ans CSS. 102 | # It should be appended to the Sprockets' precompile list. Packages' JS and CSS 103 | # are meant to be included into application.js/.css and not to be compiled 104 | # alone (you can add them by hand though). 105 | # @note Avoid importing everything from a package as it'll be precompiled and accessible 106 | # publicly. 107 | # @see Package#import_paths 108 | # @see Package#non_js_css_logical_paths 109 | # @since 0.3.0 110 | def non_js_css_logical_paths 111 | packages.flat_map(&:non_js_css_logical_paths) 112 | end 113 | 114 | # Verifies all {#packages} 115 | # @return [void] 116 | # @raise [Errors::MissingPackages] if at least one package is not build. 117 | def verify 118 | missing = packages.reject(&:verify) 119 | 120 | if missing.any? 121 | raise Errors::MissingPackages.new(missing) 122 | end 123 | end 124 | 125 | # @return [Array] where each package name at least partially matches given name. 126 | # @since 0.7.0 127 | def find_packages_by_name(name) 128 | re = Regexp.new(name, Regexp::IGNORECASE) 129 | packages.find_all do |package| 130 | package.name =~ re 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/torba/package.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | require "torba/css_url_to_erb_asset_path" 4 | require "torba/import_list" 5 | 6 | module Torba 7 | module Errors 8 | class NothingToImport < StandardError 9 | attr_reader :package, :path 10 | 11 | def initialize(options) 12 | @package = options.fetch(:package) 13 | @path = options.fetch(:path) 14 | super 15 | end 16 | end 17 | end 18 | 19 | # Represents remote source with explicit paths/files to be imported (i.e. 20 | # copied from an archive, git repository etc). 21 | # Stylesheets (if any) are treated specially because static "url(...)" 22 | # definitions should be replaced with Sprockets-aware "asset_path" helpers. 23 | class Package 24 | # @return [String] short package name, acts as as namespace within Sprockets' load path. 25 | # Doesn't need to be equal to remote package name. 26 | attr_reader :name 27 | 28 | # @return instance that implements {RemoteSources::Common} 29 | attr_reader :remote_source 30 | 31 | # @return [Array] list of file paths to import (relative to remote source root). 32 | # @example Direct path to a file 33 | # ["build/underscore.js"] 34 | # @example {http://www.rubydoc.info/stdlib/core/Dir#glob-class_method Dir.glob} pattern 35 | # ["build/*.js", "**/*.css"] 36 | # @example Any file within directory (including subdirectories) 37 | # ["build/"] # same as ["build/**/*"] 38 | # @note Put in this list only files that are absolutely necessary to you as 39 | # {Manifest#non_js_css_logical_paths} depends on it. 40 | # @see #build 41 | attr_reader :import_paths 42 | 43 | # @param name [String] see {#name} 44 | # @param remote_source [#[]] see {#remote_source} 45 | # @param options [Hash] 46 | # @option options [Array] :import list assigned to {#import_paths} 47 | def initialize(name, remote_source, options = {}) 48 | @name = name 49 | @remote_source = remote_source 50 | @import_paths = (options[:import] || ["**/*"]).sort.map do |path| 51 | if path.end_with?("/") 52 | File.join(path, "**/*") 53 | else 54 | path 55 | end 56 | end 57 | end 58 | 59 | # @return [false] if package is not build. 60 | def verify 61 | built? 62 | end 63 | 64 | # Cache remote source and import specified assets to {#load_path}. 65 | # @return [void] 66 | # @note Directories explicitly specified in {#import_paths} are not preserved after importing, 67 | # i.e. resulted file tree becomes flatten. This way you can omit build specific directories 68 | # when requiring assets in your project. If you want to preserve remote source file tree, 69 | # use glob patterns without mentioning subdirectories in them. 70 | # 71 | # In addition {#name} is used as a namespace folder within {#load_path} to protect file names 72 | # clashing across packages. 73 | # 74 | # package.name #=> "datepicker" 75 | # package.import_paths #=> ["css/stylesheet.css", "js/*.js"] 76 | # Dir[package.load_path + "/**/*"] #=> ["datepicker/stylesheet.css", "datepicker/script.js"] 77 | # 78 | # package.name #=> "datepicker" 79 | # package.import_paths #=> ["**/*.{js,css}"] 80 | # Dir[package.load_path + "/**/*"] #=> ["datepicker/css/stylesheet.css", "datepicker/js/script.js"] 81 | def build 82 | return if built? 83 | process_stylesheets 84 | process_other_assets 85 | rescue 86 | remove 87 | raise 88 | end 89 | 90 | # @return [String] path where processed files of the package reside. It's located within 91 | # {Torba.home_path} directory. 92 | def load_path 93 | @load_path ||= File.join(Torba.home_path, folder_name) 94 | end 95 | 96 | # @return [Array] {https://github.com/rails/sprockets#logical-paths logical paths} that 97 | # the package contains. 98 | # @since 0.3.0 99 | def logical_paths 100 | import_list.assets.map(&:logical_path) 101 | end 102 | 103 | # @return [Array] {https://github.com/rails/sprockets#logical-paths logical paths} that the 104 | # package contains except JS ans CSS. 105 | # @since 0.3.0 106 | def non_js_css_logical_paths 107 | import_list.non_js_css_assets.map(&:logical_path) 108 | end 109 | 110 | # @return [ImportList] 111 | def import_list 112 | @import_list ||= build_import_list 113 | end 114 | 115 | # Remove self from filesystem. 116 | # @return [void] 117 | def remove 118 | FileUtils.rm_rf(load_path) 119 | end 120 | 121 | private 122 | 123 | def built? 124 | Dir.exist?(load_path) 125 | end 126 | 127 | def folder_name 128 | digest = Torba.digest(import_paths.join << remote_source.digest) 129 | "#{name}-#{digest}" 130 | end 131 | 132 | def build_import_list 133 | assets = import_paths.flat_map do |import_path| 134 | path_wo_glob_metacharacters = import_path.sub(/\*.+$/, "") 135 | 136 | assets = remote_source[import_path].map do |absolute_path, relative_path| 137 | subpath = 138 | if relative_path == import_path 139 | File.basename(relative_path) 140 | else 141 | relative_path.sub(path_wo_glob_metacharacters, "") 142 | end 143 | 144 | ImportList::Asset.new(absolute_path, with_namespace(subpath)) 145 | end 146 | 147 | if assets.empty? 148 | raise Errors::NothingToImport.new(package: name, path: import_path) 149 | end 150 | 151 | assets 152 | end 153 | 154 | ImportList.new(assets) 155 | end 156 | 157 | def process_stylesheets 158 | import_list.css_assets.each do |css_asset| 159 | content = File.read(css_asset.absolute_path) 160 | 161 | new_content = CssUrlToErbAssetPath.call(content, css_asset.absolute_path) do |asset_file_path| 162 | if File.exist?(asset_file_path) 163 | asset = import_list.find_by_absolute_path(asset_file_path) 164 | asset.logical_path 165 | else 166 | nil 167 | end 168 | end 169 | 170 | if content == new_content 171 | new_absolute_path = File.join(load_path, css_asset.logical_path) 172 | else 173 | new_absolute_path = File.join(load_path, css_asset.logical_path + ".erb") 174 | end 175 | 176 | ensure_directory(new_absolute_path) 177 | File.write(new_absolute_path, new_content) 178 | end 179 | end 180 | 181 | def process_other_assets 182 | import_list.non_css_assets.each do |asset| 183 | new_absolute_path = File.join(load_path, asset.logical_path) 184 | ensure_directory(new_absolute_path) 185 | FileUtils.cp(asset.absolute_path, new_absolute_path) 186 | end 187 | end 188 | 189 | def with_namespace(file_name) 190 | File.join(name, file_name) 191 | end 192 | 193 | def ensure_directory(file) 194 | FileUtils.mkdir_p(File.dirname(file)) 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /lib/torba/rake_task.rb: -------------------------------------------------------------------------------- 1 | require "torba" 2 | require "rake/tasklib" 3 | 4 | module Torba 5 | # Calling Torba::RakeTask.new defines a "torba:pack" Rake task that does the 6 | # same thing as `bundle exec torba pack`. Furthermore, if the :before option 7 | # is specified, the "torba:pack" task will be installed as a prerequisite to 8 | # the specified task. 9 | # 10 | # This is useful for deployment because it allows for packing as part of the 11 | # "assets:precompile" flow, which platforms like Heroku already know to 12 | # invoke. By hooking into "assets:precompile" as a prerequisite, Torba can 13 | # participate automatically, with no change to the deployment process. 14 | # 15 | # Typical use is as follows: 16 | # 17 | # # In Rakefile 18 | # require "torba/rake_task" 19 | # Torba::RakeTask.new("torba:pack", :before => "assets:precompile") 20 | # 21 | # You can also provide a block, which will be executed before packing process: 22 | # 23 | # Torba::RakeTask.new do 24 | # # some preconfiguration to be run when the "torba:pack" task is executed 25 | # end 26 | # 27 | # Note that when you require "torba/rails" in a Rails app, this Rake task is 28 | # installed for you automatically. You only need to install the task yourself 29 | # if you are using something different, like Sinatra. 30 | # 31 | class RakeTask < Rake::TaskLib 32 | attr_reader :torba_pack_task_name 33 | 34 | def initialize(name="torba:pack", options={}, &before_pack) 35 | @torba_pack_task_name = name 36 | define_task(before_pack) 37 | install_as_prerequisite(options[:before]) if options[:before] 38 | end 39 | 40 | private 41 | 42 | def define_task(before_pack_proc) 43 | desc "Download and prepare all packages defined in Torbafile" 44 | task torba_pack_task_name do 45 | before_pack_proc.call if before_pack_proc 46 | Torba.pretty_errors { Torba.pack } 47 | end 48 | end 49 | 50 | def install_as_prerequisite(other_task) 51 | ensure_task_defined(other_task) 52 | Rake::Task[other_task].enhance([torba_pack_task_name]) 53 | end 54 | 55 | def ensure_task_defined(other_task) 56 | return if Rake::Task.task_defined?(other_task) 57 | Rake::Task.define_task(other_task) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/torba/remote_sources/common.rb: -------------------------------------------------------------------------------- 1 | module Torba 2 | module RemoteSources 3 | module Common 4 | # Expands pattern inside content of the remote source. 5 | # @return [Array] where first element of the tuple is an absolute 6 | # path to cached file and second element is the same path relative to cache directory. 7 | # @param pattern [String] see {http://www.rubydoc.info/stdlib/core/Dir#glob-class_method Dir.glob} 8 | # for pattern examples 9 | # @note Unlike Dir.glob doesn't include directories. 10 | def [](pattern) 11 | ensure_cached 12 | 13 | Dir.glob(File.join(cache_path, pattern)).sort.reject{ |path| File.directory?(path) }.map do |path| 14 | [File.absolute_path(path), path.sub(/#{cache_path}\/?/, "")] 15 | end 16 | end 17 | 18 | # @return [String] unique short name used as a representation of the remote source. 19 | # Used by default as a cache folder name. 20 | def digest 21 | raise NotImplementedError 22 | end 23 | 24 | private 25 | 26 | def cache_path 27 | @cache_path ||= File.join(Torba.cache_path, digest) 28 | end 29 | 30 | def ensure_cached 31 | raise NotImplementedError 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/torba/remote_sources/get_file.rb: -------------------------------------------------------------------------------- 1 | require "tempfile" 2 | 3 | module Torba 4 | module RemoteSources 5 | # File downloading abstraction 6 | # @since 0.3.0 7 | class GetFile 8 | # @param url [String] to be downloaded. 9 | # @return [Tempfile] temporarily stored content of the URL. 10 | # @raise [Errors::ShellCommandFailed] if failed to fetch the URL 11 | def self.process(url) 12 | tempfile = Tempfile.new("torba") 13 | tempfile.close 14 | 15 | Torba.ui.info "downloading '#{url}'" 16 | 17 | command = "curl --retry 5 --retry-max-time 60 -Lf -o #{tempfile.path} #{url}" 18 | system(command) || raise(Errors::ShellCommandFailed.new(command)) 19 | 20 | tempfile 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/torba/remote_sources/github_release.rb: -------------------------------------------------------------------------------- 1 | require "torba/remote_sources/zip" 2 | 3 | module Torba 4 | module RemoteSources 5 | # Represents {https://help.github.com/articles/about-releases/ Github release}. 6 | class GithubRelease < Zip 7 | # @return [String] repository user and name. 8 | # @example 9 | # "jashkenas/underscore" 10 | # @see #repository_name 11 | # @see #repository_user 12 | attr_reader :source 13 | 14 | # @return [String] 15 | # @since 0.2.0 16 | attr_reader :repository_name 17 | 18 | # @return [String] 19 | # @since 0.2.0 20 | attr_reader :repository_user 21 | 22 | # @return [String] repository tag. 23 | # @example 24 | # "v1.8.3" 25 | attr_reader :tag 26 | 27 | # @param source see {#source} 28 | # @param tag see {#tag} 29 | def initialize(source, tag) 30 | @source = source 31 | @tag = tag 32 | @repository_user, @repository_name = source.split("/") 33 | super("https://github.com/#{source}/archive/#{tag}.zip") 34 | @digest = "#{repository_name}-#{Torba.digest(url)}" 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/torba/remote_sources/npm.rb: -------------------------------------------------------------------------------- 1 | require "torba/remote_sources/targz" 2 | 3 | module Torba 4 | module RemoteSources 5 | # Represents {https://npmjs.com npm package}. 6 | # @since 0.3.0 7 | class Npm < Targz 8 | # @return [String] package name. 9 | # @example 10 | # "coffee-script" 11 | # "@lottiefiles/lottie-player" 12 | attr_reader :package 13 | 14 | # @return [String] package version. 15 | # @example 16 | # "1.8.3" 17 | attr_reader :version 18 | 19 | # @param package see {#package} 20 | # @param version see {#version} 21 | def initialize(package, version) 22 | @package = package 23 | @version = version 24 | 25 | # https://docs.npmjs.com/about-scopes 26 | # "@lottiefiles/lottie-player" => "lottie-player" 27 | unscoped_package = package.sub(%r{@[^/]+/}, "") 28 | 29 | super("https://registry.npmjs.org/#{package}/-/#{unscoped_package}-#{version}.tgz") 30 | @digest = "#{package}-#{Torba.digest(url)}" 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/torba/remote_sources/targz.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | require "torba/remote_sources/common" 4 | require "torba/remote_sources/get_file" 5 | 6 | module Torba 7 | module RemoteSources 8 | # Represents remote tar.gz archive. 9 | # @since 0.3.0 10 | class Targz 11 | include Common 12 | 13 | attr_reader :url, :digest 14 | 15 | def initialize(url) 16 | @url = url 17 | @digest = "#{File.basename(url).sub(/\.(tgz|tar\.gz)$/, '')}-#{Torba.digest(url)}" 18 | end 19 | 20 | private 21 | 22 | def ensure_cached 23 | unless Dir.exist?(cache_path) 24 | FileUtils.mkdir_p(cache_path) 25 | 26 | tempfile = GetFile.process(url) 27 | 28 | command = "gzip -qcd #{tempfile.path} | tar --no-same-owner -mxpf - --strip-components=1 -C #{cache_path}" 29 | system(command) || raise(Errors::ShellCommandFailed.new(command)) 30 | end 31 | rescue 32 | FileUtils.rm_rf(cache_path) 33 | raise 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/torba/remote_sources/zip.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | require "torba/remote_sources/common" 4 | require "torba/remote_sources/get_file" 5 | 6 | module Torba 7 | module RemoteSources 8 | # Represents remote zip archive. 9 | class Zip 10 | include Common 11 | 12 | attr_reader :url, :digest 13 | 14 | def initialize(url) 15 | @url = url 16 | @digest = "#{File.basename url, '.zip'}-#{Torba.digest(url)}" 17 | end 18 | 19 | private 20 | 21 | def ensure_cached 22 | unless Dir.exist?(cache_path) 23 | FileUtils.mkdir_p(cache_path) 24 | 25 | tempfile = GetFile.process(url) 26 | 27 | command = "unzip -oqq -d #{cache_path} #{tempfile.path}" 28 | system(command) || raise(Errors::ShellCommandFailed.new(command)) 29 | 30 | get_rid_of_top_level_directory 31 | end 32 | rescue 33 | FileUtils.rm_rf(cache_path) 34 | raise 35 | end 36 | 37 | def get_rid_of_top_level_directory 38 | top_level_content = Dir.glob("#{cache_path}/*") 39 | if top_level_content.size == 1 && File.directory?(top_level_content.first) 40 | top_level_dir = top_level_content.first 41 | FileUtils.cp_r("#{top_level_dir}/.", cache_path) 42 | FileUtils.rm_rf(top_level_dir) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/torba/ui.rb: -------------------------------------------------------------------------------- 1 | require "thor/shell" 2 | 3 | module Torba 4 | # Thin wrapper around Thor::Shell. 5 | class Ui 6 | def initialize 7 | @shell = Thor::Base.shell.new 8 | end 9 | 10 | def info(message) 11 | @shell.say(message) 12 | end 13 | 14 | def confirm(message) 15 | @shell.say(message, :green) 16 | end 17 | 18 | def suggest(message) 19 | @shell.say(message, :yellow) 20 | end 21 | 22 | def error(message) 23 | @shell.say(message, :red) 24 | end 25 | 26 | # @return [Integer] index of chosen option. 27 | # @return [nil] if exit was chosen. 28 | # @since 0.7.0 29 | def choose_one(options) 30 | options.each_with_index do |option, index| 31 | info("#{index + 1} : #{option}") 32 | end 33 | info("0 : - exit -") 34 | 35 | index = @shell.ask("> ").to_i - 1 36 | (0..options.size - 1).cover?(index) ? index : nil 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/torba/verify.rb: -------------------------------------------------------------------------------- 1 | require "torba" 2 | 3 | if STDOUT.tty? 4 | Torba.pretty_errors { Torba.verify } 5 | else 6 | Torba.verify 7 | end 8 | -------------------------------------------------------------------------------- /test/acceptance-cli/open_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Torba 4 | class OpenTest < Minitest::Test 5 | def test_complains_no_editor_set 6 | out, err, status = torba("open trumbowyg", torbafile: "01_zip.rb", env: { 7 | "TORBA_EDITOR" => nil, "VISUAL" => nil, "EDITOR" => nil 8 | }) 9 | refute status.success?, err 10 | assert_includes out, "To open a package, set $EDITOR or $TORBA_EDITOR" 11 | end 12 | 13 | def test_runs_pack 14 | out, err, status = torba("open trumbowyg", torbafile: "01_zip.rb", env: { 15 | "TORBA_EDITOR" => "echo torba_editor", "VISUAL" => "echo visual", "EDITOR" => "echo editor" 16 | }) 17 | assert status.success?, err 18 | assert_dirs_equal "test/fixtures/home_path/01", path_to_packaged("trumbowyg") 19 | end 20 | 21 | def test_uses_torba_editor_first 22 | out, err, status = torba("open trumbowyg", torbafile: "01_zip.rb", env: { 23 | "TORBA_EDITOR" => "echo torba_editor", "VISUAL" => "echo visual", "EDITOR" => "echo editor" 24 | }) 25 | assert status.success?, err 26 | assert_includes out, "torba_editor #{path_to_packaged "trumbowyg"}" 27 | end 28 | 29 | def test_uses_visual_editor_second 30 | out, err, status = torba("open trumbowyg", torbafile: "01_zip.rb", env: { 31 | "TORBA_EDITOR" => nil, "VISUAL" => "echo visual", "EDITOR" => "echo editor" 32 | }) 33 | assert status.success?, err 34 | assert_includes out, "visual #{path_to_packaged "trumbowyg"}" 35 | end 36 | 37 | def test_uses_editor_third 38 | out, err, status = torba("open trumbowyg", torbafile: "01_zip.rb", env: { 39 | "TORBA_EDITOR" => nil, "VISUAL" => nil, "EDITOR" => "echo editor" 40 | }) 41 | assert status.success?, err 42 | assert_includes out, "editor #{path_to_packaged "trumbowyg"}" 43 | end 44 | 45 | def test_finds_by_partial_package_name 46 | out, err, status = torba("open trumbo", torbafile: "01_zip.rb", env: { 47 | "TORBA_EDITOR" => nil, "VISUAL" => nil, "EDITOR" => "echo editor" 48 | }) 49 | assert status.success?, err 50 | assert_includes out, "editor #{path_to_packaged "trumbowyg"}" 51 | end 52 | 53 | def test_could_not_find_package 54 | out, err, status = torba("open dumbo", torbafile: "01_zip.rb", env: { 55 | "TORBA_EDITOR" => nil, "VISUAL" => nil, "EDITOR" => "echo editor" 56 | }) 57 | refute status.success?, err 58 | assert_includes out, "Could not find package 'dumbo'." 59 | end 60 | 61 | def test_similar_names_show_options 62 | out, err, status = torba("open bourbon", torbafile: "04_similar_names.rb", env: { 63 | "TORBA_EDITOR" => nil, "VISUAL" => nil, "EDITOR" => "echo editor" 64 | }) 65 | refute status.success?, err 66 | assert_includes out, < nil, "VISUAL" => nil, "EDITOR" => "echo editor" 77 | }) 78 | assert status.success?, err 79 | refute_includes out, "editor #{path_to_packaged "bourbon"}" 80 | assert_includes out, "editor #{path_to_packaged "bourbon-neat"}" 81 | end 82 | 83 | def test_similar_names_chosen_exit 84 | out, err, status = torba("open bourbon", torbafile: "04_similar_names.rb", stdin_data: "0", env: { 85 | "TORBA_EDITOR" => nil, "VISUAL" => nil, "EDITOR" => "echo editor" 86 | }) 87 | refute status.success?, err 88 | refute_includes out, "editor #{path_to_packaged "bourbon"}" 89 | refute_includes out, "editor #{path_to_packaged "bourbon-neat"}" 90 | end 91 | 92 | def test_similar_names_chosen_unexisted_option 93 | out, err, status = torba("open bourbon", torbafile: "04_similar_names.rb", stdin_data: "7", env: { 94 | "TORBA_EDITOR" => nil, "VISUAL" => nil, "EDITOR" => "echo editor" 95 | }) 96 | refute status.success?, err 97 | refute_includes out, "editor #{path_to_packaged "bourbon"}" 98 | refute_includes out, "editor #{path_to_packaged "bourbon-neat"}" 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/acceptance-cli/pack_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Torba 4 | class PackTest < Minitest::Test 5 | def test_zip 6 | out, err, status = torba("pack", torbafile: "01_zip.rb") 7 | assert status.success?, err 8 | assert_includes out, "Torba has been packed!" 9 | assert_dirs_equal "test/fixtures/home_path/01", path_to_packaged("trumbowyg") 10 | end 11 | 12 | def test_targz 13 | out, err, status = torba("pack", torbafile: "01_targz.rb") 14 | assert status.success?, err 15 | assert_includes out, "Torba has been packed!" 16 | assert_dirs_equal "test/fixtures/home_path/01", path_to_packaged("trumbowyg") 17 | end 18 | 19 | def test_gh_release 20 | out, err, status = torba("pack", torbafile: "01_gh_release.rb") 21 | assert status.success?, err 22 | assert_includes out, "Torba has been packed!" 23 | assert_dirs_equal "test/fixtures/home_path/01", path_to_packaged("trumbowyg") 24 | end 25 | 26 | def test_npm 27 | out, err, status = torba("pack", torbafile: "02_npm.rb") 28 | assert status.success?, err 29 | assert_includes out, "Torba has been packed!" 30 | assert_dirs_equal "test/fixtures/home_path/02", path_to_packaged("lo_dash") 31 | end 32 | 33 | def test_without_image_asset_specified_in_import 34 | out, err, status = torba("pack", torbafile: "01_image_asset_not_specified.rb") 35 | refute status.success?, err 36 | assert_includes out, <');", new_content 49 | end 50 | 51 | def test_url_double_quotes 52 | current_file = "/home/package/dist/ui/stylesheet.css" 53 | css = 'background-image: url("icons.png");' 54 | 55 | filter.call(css, current_file) do |image_full_url| 56 | assert_equal "/home/package/dist/ui/icons.png", image_full_url 57 | end 58 | end 59 | 60 | def test_minified 61 | current_file = "/home/package/dist/ui/stylesheet.css" 62 | css = ".image{background-image:url(icons.png)}.border{border(1px solid)};" 63 | 64 | filter.call(css, current_file) do |image_full_url| 65 | assert_equal "/home/package/dist/ui/icons.png", image_full_url 66 | end 67 | end 68 | 69 | def test_stylesheet_and_image_in_sibling_directories 70 | current_file = "/home/package/dist/ui/stylesheet.css" 71 | css = "background-image: url('../images/icons.png');" 72 | 73 | new_content = filter.call(css, current_file) do |image_full_url| 74 | assert_equal "/home/package/dist/images/icons.png", image_full_url 75 | "images/icons.png" 76 | end 77 | assert_equal "background-image: url('<%= asset_path('images/icons.png') %>');", new_content 78 | end 79 | 80 | def test_image_in_child_directory_of_stylesheet 81 | current_file = "/home/package/dist/ui/stylesheet.css" 82 | css = "background-image: url('./images/icons.png');" 83 | 84 | new_content = filter.call(css, current_file) do |image_full_url| 85 | assert_equal "/home/package/dist/ui/images/icons.png", image_full_url 86 | "ui/images/icons.png" 87 | end 88 | assert_equal "background-image: url('<%= asset_path('ui/images/icons.png') %>');", new_content 89 | end 90 | 91 | def test_url_with_iefix_query 92 | current_file = "/home/dist/ui/stylesheet.css" 93 | css = <<-CSS 94 | @font-face { 95 | font-family: "Material-Design-Icons"; 96 | src: url("../font/material-design-icons/Material-Design-Icons.eot?#iefix") format("embedded-opentype"); 97 | font-weight: normal; 98 | font-style: normal; } 99 | CSS 100 | 101 | new_content = filter.call(css, current_file) do |image_full_url| 102 | assert_equal "/home/dist/font/material-design-icons/Material-Design-Icons.eot", image_full_url 103 | "material-design-icons/Material-Design-Icons.eot" 104 | end 105 | 106 | assert_equal <<-CSS, new_content 107 | @font-face { 108 | font-family: "Material-Design-Icons"; 109 | src: url("<%= asset_path('material-design-icons/Material-Design-Icons.eot') %>?#iefix") format("embedded-opentype"); 110 | font-weight: normal; 111 | font-style: normal; } 112 | CSS 113 | end 114 | 115 | def test_url_with_svg_fragment 116 | current_file = "/home/dist/ui/stylesheet.css" 117 | css = <<-CSS 118 | @font-face { 119 | font-family: "Material-Design-Icons"; 120 | src: url("../font/material-design-icons/Material-Design-Icons.svg#Material-Design-Icons") format("svg"); 121 | font-weight: normal; 122 | font-style: normal; } 123 | CSS 124 | 125 | new_content = filter.call(css, current_file) do |image_full_url| 126 | assert_equal "/home/dist/font/material-design-icons/Material-Design-Icons.svg", image_full_url 127 | "material-design-icons/Material-Design-Icons.svg" 128 | end 129 | 130 | assert_equal <<-CSS, new_content 131 | @font-face { 132 | font-family: "Material-Design-Icons"; 133 | src: url("<%= asset_path('material-design-icons/Material-Design-Icons.svg') %>#Material-Design-Icons") format("svg"); 134 | font-weight: normal; 135 | font-style: normal; } 136 | CSS 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /test/fixtures/home_path/01/trumbowyg/icons-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torba-rb/torba/b06cbb15e4e58a718fb339e0365156d06c4574d8/test/fixtures/home_path/01/trumbowyg/icons-2x.png -------------------------------------------------------------------------------- /test/fixtures/home_path/01/trumbowyg/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torba-rb/torba/b06cbb15e4e58a718fb339e0365156d06c4574d8/test/fixtures/home_path/01/trumbowyg/icons.png -------------------------------------------------------------------------------- /test/fixtures/home_path/01/trumbowyg/trumbowyg.css.erb: -------------------------------------------------------------------------------- 1 | /** 2 | * Trumbowyg v1.1.6 - A lightweight WYSIWYG editor 3 | * Default stylesheet for Trumbowyg editor 4 | * ------------------------ 5 | * @link http://alex-d.github.io/Trumbowyg 6 | * @license MIT 7 | * @author Alexandre Demode (Alex-D) 8 | * Twitter : @AlexandreDemode 9 | * Website : alex-d.fr 10 | */ 11 | 12 | .trumbowyg-box, .trumbowyg-editor { 13 | display: block; 14 | position: relative; 15 | border: 1px solid #DDD; 16 | width: 96%; 17 | min-height: 300px; 18 | margin: 17px auto; } 19 | 20 | .trumbowyg-box .trumbowyg-editor { 21 | margin: 0 auto; } 22 | 23 | .trumbowyg-box.trumbowyg-fullscreen { 24 | background: #FEFEFE; } 25 | 26 | .trumbowyg-editor, .trumbowyg-textarea { 27 | position: relative; 28 | -webkit-box-sizing: border-box; 29 | -moz-box-sizing: border-box; 30 | box-sizing: border-box; 31 | padding: 1% 2%; 32 | min-height: 300px; 33 | width: 100%; 34 | border-style: none; 35 | resize: none; 36 | outline: none; } 37 | 38 | .trumbowyg-box-blur .trumbowyg-editor * { 39 | color: transparent !important; 40 | text-shadow: 0 0 7px #333; } 41 | .trumbowyg-box-blur .trumbowyg-editor img { 42 | opacity: 0.2; } 43 | 44 | .trumbowyg-textarea { 45 | position: relative; 46 | display: block; 47 | overflow: auto; 48 | border: none; 49 | white-space: normal; } 50 | 51 | .trumbowyg-editor[contenteditable=true]:empty:before { 52 | content: attr(placeholder); 53 | color: #999; } 54 | 55 | .trumbowyg-button-pane { 56 | position: relative; 57 | width: 100%; 58 | background: #ecf0f1; 59 | border-bottom: 1px solid #d7e0e2; 60 | margin: 0; 61 | padding: 0; 62 | list-style-type: none; 63 | line-height: 10px; 64 | -webkit-backface-visibility: hidden; 65 | backface-visibility: hidden; } 66 | .trumbowyg-button-pane li { 67 | display: inline-block; 68 | text-align: center; 69 | overflow: hidden; } 70 | .trumbowyg-button-pane li.trumbowyg-separator { 71 | width: 1px; 72 | background: #d7e0e2; 73 | margin: 0 5px; 74 | height: 35px; } 75 | .trumbowyg-button-pane.trumbowyg-disable li:not(.trumbowyg-not-disable) button:not(.trumbowyg-active) { 76 | opacity: 0.2; 77 | cursor: default; } 78 | .trumbowyg-button-pane.trumbowyg-disable li.trumbowyg-separator { 79 | background: #e3e9eb; } 80 | .trumbowyg-button-pane:not(.trumbowyg-disable) li button:hover, .trumbowyg-button-pane:not(.trumbowyg-disable) li button:focus, .trumbowyg-button-pane li button.trumbowyg-active, .trumbowyg-button-pane li.trumbowyg-not-disable button:hover, .trumbowyg-button-pane li.trumbowyg-not-disable button:focus { 81 | background-color: #FFF; 82 | outline: none; } 83 | .trumbowyg-button-pane li .trumbowyg-open-dropdown:after { 84 | display: block; 85 | content: " "; 86 | position: absolute; 87 | top: 25px; 88 | right: 3px; 89 | height: 0; 90 | width: 0; 91 | border: 3px solid transparent; 92 | border-top-color: #555; } 93 | .trumbowyg-button-pane .trumbowyg-buttons-right { 94 | float: right; 95 | width: auto; } 96 | .trumbowyg-button-pane .trumbowyg-buttons-right button { 97 | float: left; } 98 | 99 | .trumbowyg-dropdown { 100 | width: 200px; 101 | border: 1px solid #ecf0f1; 102 | padding: 5px 0; 103 | border-top: none; 104 | background: #FFF; 105 | margin-left: -1px; 106 | -webkit-box-shadow: rgba(0, 0, 0, 0.1) 0 2px 3px; 107 | box-shadow: rgba(0, 0, 0, 0.1) 0 2px 3px; } 108 | .trumbowyg-dropdown button { 109 | display: block; 110 | width: 100%; 111 | height: 35px; 112 | line-height: 35px; 113 | text-decoration: none; 114 | background: #FFF; 115 | padding: 0 14px; 116 | color: #333; 117 | border: none; 118 | cursor: pointer; 119 | text-align: left; 120 | font-size: 15px; 121 | -webkit-transition: all 0.15s; 122 | -o-transition: all 0.15s; 123 | transition: all 0.15s; } 124 | .trumbowyg-dropdown button:hover, .trumbowyg-dropdown button:focus { 125 | background: #ecf0f1; } 126 | 127 | /* Modal box */ 128 | .trumbowyg-modal { 129 | position: absolute; 130 | top: 0; 131 | left: 50%; 132 | margin-left: -260px; 133 | width: 520px; 134 | height: 290px; 135 | overflow: hidden; } 136 | 137 | .trumbowyg-modal-box { 138 | position: absolute; 139 | top: 0; 140 | left: 50%; 141 | margin-left: -250px; 142 | width: 500px; 143 | height: 275px; 144 | z-index: 1; 145 | background-color: #FFF; 146 | text-align: center; 147 | -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 3px; 148 | box-shadow: rgba(0, 0, 0, 0.2) 0 2px 3px; 149 | -webkit-backface-visibility: hidden; 150 | backface-visibility: hidden; } 151 | .trumbowyg-modal-box .trumbowyg-modal-title { 152 | font-size: 24px; 153 | font-weight: bold; 154 | margin: 0 0 20px; 155 | padding: 15px 0 13px; 156 | display: block; 157 | border-bottom: 1px solid #EEE; 158 | color: #333; 159 | background: #fbfcfc; } 160 | .trumbowyg-modal-box .trumbowyg-progress { 161 | width: 100%; 162 | background: #F00; 163 | height: 3px; 164 | position: absolute; 165 | top: 58px; } 166 | .trumbowyg-modal-box .trumbowyg-progress .trumbowyg-progress-bar { 167 | background: #2BC06A; 168 | height: 100%; 169 | -webkit-transition: width 0.15s linear; 170 | -o-transition: width 0.15s linear; 171 | transition: width 0.15s linear; } 172 | .trumbowyg-modal-box label { 173 | display: block; 174 | position: relative; 175 | margin: 15px 12px; 176 | height: 27px; 177 | line-height: 27px; 178 | overflow: hidden; } 179 | .trumbowyg-modal-box label .trumbowyg-input-infos { 180 | display: block; 181 | text-align: left; 182 | height: 25px; 183 | line-height: 25px; 184 | -webkit-transition: all 0.15; 185 | -o-transition: all 0.15; 186 | transition: all 0.15; } 187 | .trumbowyg-modal-box label .trumbowyg-input-infos span { 188 | display: block; 189 | color: #859fa5; 190 | background-color: #fbfcfc; 191 | border: 1px solid #DEDEDE; 192 | padding: 0 2%; 193 | width: 19.5%; } 194 | .trumbowyg-modal-box label .trumbowyg-input-infos span.trumbowyg-msg-error { 195 | color: #e74c3c; } 196 | .trumbowyg-modal-box label.trumbowyg-input-error input, .trumbowyg-modal-box label.trumbowyg-input-error textarea { 197 | border: 1px solid #e74c3c; } 198 | .trumbowyg-modal-box label.trumbowyg-input-error .trumbowyg-input-infos { 199 | margin-top: -27px; } 200 | .trumbowyg-modal-box label input { 201 | position: absolute; 202 | top: 0; 203 | right: 0; 204 | height: 25px; 205 | line-height: 25px; 206 | border: 1px solid #DEDEDE; 207 | background: transparent; 208 | width: 72%; 209 | padding: 0 2%; 210 | margin: 0 0 0 23%; 211 | -webkit-transition: all 0.15s; 212 | -o-transition: all 0.15s; 213 | transition: all 0.15s; } 214 | .trumbowyg-modal-box label input:hover, .trumbowyg-modal-box label input:focus { 215 | outline: none; 216 | border: 1px solid #95a5a6; } 217 | .trumbowyg-modal-box label input:focus { 218 | background: rgba(230, 230, 255, 0.1); } 219 | .trumbowyg-modal-box .error { 220 | margin-top: 25px; 221 | display: block; 222 | color: red; } 223 | .trumbowyg-modal-box .trumbowyg-modal-button { 224 | position: absolute; 225 | bottom: 10px; 226 | right: 0; 227 | text-decoration: none; 228 | color: #FFF; 229 | display: block; 230 | width: 100px; 231 | height: 35px; 232 | line-height: 33px; 233 | margin: 0 10px; 234 | background-color: #333; 235 | border: none; 236 | border-top: none; 237 | cursor: pointer; 238 | font-family: "Trebuchet MS", Helvetica, Verdana, sans-serif; 239 | font-size: 16px; 240 | -webkit-transition: all 0.15s; 241 | -o-transition: all 0.15s; 242 | transition: all 0.15s; } 243 | .trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-submit { 244 | right: 110px; 245 | background: #2bc069; } 246 | .trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-submit:hover, .trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-submit:focus { 247 | background: #40d47d; 248 | outline: none; } 249 | .trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-submit:active { 250 | background: #25a259; } 251 | .trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-reset { 252 | color: #555; 253 | background: #e6e6e6; } 254 | .trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-reset:hover, .trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-reset:focus { 255 | background: #fbfbfb; 256 | outline: none; } 257 | .trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-reset:active { 258 | background: #d4d4d4; } 259 | 260 | .trumbowyg-overlay { 261 | position: absolute; 262 | background-color: rgba(255, 255, 255, 0.5); 263 | width: 100%; 264 | left: 0; 265 | display: none; } 266 | 267 | /** 268 | * Fullscreen 269 | */ 270 | .trumbowyg-fullscreen { 271 | position: fixed; 272 | top: 0; 273 | left: 0; 274 | width: 100%; 275 | height: 100%; 276 | margin: 0; 277 | padding: 0; 278 | z-index: 99999; } 279 | .trumbowyg-fullscreen.trumbowyg-box, .trumbowyg-fullscreen .trumbowyg-editor { 280 | border: none; } 281 | .trumbowyg-fullscreen .trumbowyg-overlay { 282 | height: 100% !important; } 283 | 284 | /* 285 | * Reset for resetCss option 286 | */ 287 | .trumbowyg-editor object, .trumbowyg-editor embed, .trumbowyg-editor video, .trumbowyg-editor img { 288 | width: auto; 289 | max-width: 100%; } 290 | .trumbowyg-editor video, .trumbowyg-editor img { 291 | height: auto; } 292 | .trumbowyg-editor img { 293 | cursor: move; } 294 | .trumbowyg-editor.trumbowyg-reset-css { 295 | background: #FEFEFE !important; 296 | font-family: "Trebuchet MS", Helvetica, Verdana, sans-serif !important; 297 | font-size: 14px !important; 298 | line-height: 1.45em !important; 299 | white-space: normal !important; 300 | color: #333; } 301 | .trumbowyg-editor.trumbowyg-reset-css a { 302 | color: #15c !important; 303 | text-decoration: underline !important; } 304 | .trumbowyg-editor.trumbowyg-reset-css div, .trumbowyg-editor.trumbowyg-reset-css p, .trumbowyg-editor.trumbowyg-reset-css ul, .trumbowyg-editor.trumbowyg-reset-css ol, .trumbowyg-editor.trumbowyg-reset-css blockquote { 305 | -webkit-box-shadow: none !important; 306 | box-shadow: none !important; 307 | background: none !important; 308 | margin: 0 !important; 309 | margin-bottom: 15px !important; 310 | line-height: 1.4em !important; 311 | font-family: "Trebuchet MS", Helvetica, Verdana, sans-serif !important; 312 | font-size: 14px !important; 313 | border: none; } 314 | .trumbowyg-editor.trumbowyg-reset-css iframe, .trumbowyg-editor.trumbowyg-reset-css object, .trumbowyg-editor.trumbowyg-reset-css hr { 315 | margin-bottom: 15px !important; } 316 | .trumbowyg-editor.trumbowyg-reset-css blockquote { 317 | margin-left: 32px !important; 318 | font-style: italic !important; 319 | color: #555; } 320 | .trumbowyg-editor.trumbowyg-reset-css ul, .trumbowyg-editor.trumbowyg-reset-css ol { 321 | padding-left: 20px !important; } 322 | .trumbowyg-editor.trumbowyg-reset-css ul ul, .trumbowyg-editor.trumbowyg-reset-css ol ol, .trumbowyg-editor.trumbowyg-reset-css ul ol, .trumbowyg-editor.trumbowyg-reset-css ol ul { 323 | border: none; 324 | margin: 2px !important; 325 | padding: 0 !important; 326 | padding-left: 24px !important; } 327 | .trumbowyg-editor.trumbowyg-reset-css hr { 328 | display: block; 329 | height: 1px; 330 | border: none; 331 | border-top: 1px solid #CCC; } 332 | .trumbowyg-editor.trumbowyg-reset-css h1, .trumbowyg-editor.trumbowyg-reset-css h2, .trumbowyg-editor.trumbowyg-reset-css h3, .trumbowyg-editor.trumbowyg-reset-css h4 { 333 | color: #111; 334 | background: none; 335 | margin: 0 !important; 336 | padding: 0 !important; 337 | font-weight: bold; } 338 | .trumbowyg-editor.trumbowyg-reset-css h1 { 339 | font-size: 32px !important; 340 | line-height: 38px !important; 341 | margin-bottom: 20px !important; } 342 | .trumbowyg-editor.trumbowyg-reset-css h2 { 343 | font-size: 26px !important; 344 | line-height: 34px !important; 345 | margin-bottom: 15px !important; } 346 | .trumbowyg-editor.trumbowyg-reset-css h3 { 347 | font-size: 22px !important; 348 | line-height: 28px !important; 349 | margin-bottom: 7px !important; } 350 | .trumbowyg-editor.trumbowyg-reset-css h4 { 351 | font-size: 16px !important; 352 | line-height: 22px !important; 353 | margin-bottom: 7px !important; } 354 | 355 | /* 356 | * Buttons icons 357 | */ 358 | .trumbowyg-button-pane li button { 359 | display: block; 360 | position: relative; 361 | text-indent: -9999px; 362 | width: 35px; 363 | height: 35px; 364 | overflow: hidden; 365 | background: transparent url("<%= asset_path('trumbowyg/icons.png') %>") no-repeat; 366 | border: none; 367 | cursor: pointer; 368 | -webkit-transition: background-color 0.15s, background-image 0.15s, opacity 0.15s; 369 | -o-transition: background-color 0.15s, background-image 0.15s, opacity 0.15s; 370 | transition: background-color 0.15s, background-image 0.15s, opacity 0.15s; 371 | /* English and others */ } 372 | .trumbowyg-button-pane li button.trumbowyg-viewHTML-button { 373 | background-position: 5px -545px; } 374 | .trumbowyg-button-pane li button.trumbowyg-formatting-button { 375 | background-position: 5px -120px; } 376 | .trumbowyg-button-pane li button.trumbowyg-bold-button, .trumbowyg-button-pane li button.trumbowyg-strong-button { 377 | background-position: 5px -45px; } 378 | .trumbowyg-button-pane li button.trumbowyg-italic-button, .trumbowyg-button-pane li button.trumbowyg-em-button { 379 | background-position: 5px 5px; } 380 | .trumbowyg-button-pane li button.trumbowyg-underline-button { 381 | background-position: 5px -470px; } 382 | .trumbowyg-button-pane li button.trumbowyg-strikethrough-button, .trumbowyg-button-pane li button.trumbowyg-del-button { 383 | background-position: 5px -445px; } 384 | .trumbowyg-button-pane li button.trumbowyg-link-button { 385 | background-position: 5px -345px; } 386 | .trumbowyg-button-pane li button.trumbowyg-insertImage-button { 387 | background-position: 5px -245px; } 388 | .trumbowyg-button-pane li button.trumbowyg-justifyLeft-button { 389 | background-position: 5px -320px; } 390 | .trumbowyg-button-pane li button.trumbowyg-justifyCenter-button { 391 | background-position: 5px -70px; } 392 | .trumbowyg-button-pane li button.trumbowyg-justifyRight-button { 393 | background-position: 5px -395px; } 394 | .trumbowyg-button-pane li button.trumbowyg-justifyFull-button { 395 | background-position: 5px -295px; } 396 | .trumbowyg-button-pane li button.trumbowyg-unorderedList-button { 397 | background-position: 5px -495px; } 398 | .trumbowyg-button-pane li button.trumbowyg-orderedList-button { 399 | background-position: 5px -370px; } 400 | .trumbowyg-button-pane li button.trumbowyg-horizontalRule-button { 401 | background-position: 5px -220px; } 402 | .trumbowyg-button-pane li button.trumbowyg-fullscreen-button { 403 | background-position: 5px -170px; } 404 | .trumbowyg-button-pane li button.trumbowyg-close-button { 405 | background-position: 5px -95px; } 406 | 407 | .trumbowyg-fullscreen .trumbowyg-button-pane li button.trumbowyg-fullscreen-button { 408 | background-position: 5px -145px; } 409 | 410 | .trumbowyg-button-pane li:first-child button { 411 | margin-left: 6px; } 412 | .trumbowyg-button-pane li:last-child button { 413 | margin-right: 6px; } 414 | 415 | /* French */ 416 | .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-bold-button, .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-strong-button { 417 | background-position: 5px -195px; } 418 | .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-underline-button { 419 | background-position: 5px -420px; } 420 | .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-strikethrough-button, .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-del-button { 421 | background-position: 5px -270px; } 422 | 423 | @media only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 4/3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { 424 | /* French */ 425 | .trumbowyg-button-pane li button { 426 | -webkit-background-size: 25px 575px !important; 427 | background-size: 25px 575px !important; 428 | background-image: url("<%= asset_path('trumbowyg/icons-2x.png') %>") !important; 429 | /* English and others */ } 430 | .trumbowyg-button-pane li button.trumbowyg-viewHTML-button { 431 | background-position: 5px -545px; } 432 | .trumbowyg-button-pane li button.trumbowyg-formatting-button { 433 | background-position: 5px -120px; } 434 | .trumbowyg-button-pane li button.trumbowyg-bold-button, .trumbowyg-button-pane li button.trumbowyg-strong-button { 435 | background-position: 5px -45px; } 436 | .trumbowyg-button-pane li button.trumbowyg-italic-button, .trumbowyg-button-pane li button.trumbowyg-em-button { 437 | background-position: 5px 5px; } 438 | .trumbowyg-button-pane li button.trumbowyg-underline-button { 439 | background-position: 5px -470px; } 440 | .trumbowyg-button-pane li button.trumbowyg-strikethrough-button, .trumbowyg-button-pane li button.trumbowyg-del-button { 441 | background-position: 5px -445px; } 442 | .trumbowyg-button-pane li button.trumbowyg-link-button { 443 | background-position: 5px -345px; } 444 | .trumbowyg-button-pane li button.trumbowyg-insertImage-button { 445 | background-position: 5px -245px; } 446 | .trumbowyg-button-pane li button.trumbowyg-justifyLeft-button { 447 | background-position: 5px -320px; } 448 | .trumbowyg-button-pane li button.trumbowyg-justifyCenter-button { 449 | background-position: 5px -70px; } 450 | .trumbowyg-button-pane li button.trumbowyg-justifyRight-button { 451 | background-position: 5px -395px; } 452 | .trumbowyg-button-pane li button.trumbowyg-justifyFull-button { 453 | background-position: 5px -295px; } 454 | .trumbowyg-button-pane li button.trumbowyg-unorderedList-button { 455 | background-position: 5px -495px; } 456 | .trumbowyg-button-pane li button.trumbowyg-orderedList-button { 457 | background-position: 5px -370px; } 458 | .trumbowyg-button-pane li button.trumbowyg-horizontalRule-button { 459 | background-position: 5px -220px; } 460 | .trumbowyg-button-pane li button.trumbowyg-fullscreen-button { 461 | background-position: 5px -170px; } 462 | .trumbowyg-button-pane li button.trumbowyg-close-button { 463 | background-position: 5px -95px; } 464 | .trumbowyg-fullscreen .trumbowyg-button-pane li a.trumbowyg-fullscreen-button { 465 | background-position: 5px -145px; } 466 | .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-bold-button, .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-strong-button { 467 | background-position: 5px -195px; } 468 | .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-underline-button { 469 | background-position: 5px -420px; } 470 | .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-strikethrough-button, .trumbowyg-fr .trumbowyg-button-pane li button.trumbowyg-del-button { 471 | background-position: 5px -270px; } } 472 | -------------------------------------------------------------------------------- /test/fixtures/home_path/01/trumbowyg/trumbowyg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Trumbowyg v1.1.5 - A lightweight WYSIWYG editor 3 | * Trumbowyg core file 4 | * ------------------------ 5 | * @link http://alex-d.github.io/Trumbowyg 6 | * @license MIT 7 | * @author Alexandre Demode (Alex-D) 8 | * Twitter : @AlexandreDemode 9 | * Website : alex-d.fr 10 | */ 11 | 12 | jQuery.trumbowyg = { 13 | langs: { 14 | en: { 15 | viewHTML: "View HTML", 16 | 17 | formatting: "Formatting", 18 | p: "Paragraph", 19 | blockquote: "Quote", 20 | code: "Code", 21 | header: "Header", 22 | 23 | bold: "Bold", 24 | italic: "Italic", 25 | strikethrough: "Stroke", 26 | underline: "Underline", 27 | 28 | strong: "Strong", 29 | em: "Emphasis", 30 | del: "Deleted", 31 | 32 | unorderedList: "Unordered list", 33 | orderedList: "Ordered list", 34 | 35 | insertImage: "Insert Image", 36 | insertVideo: "Insert Video", 37 | link: "Link", 38 | createLink: "Insert link", 39 | unlink: "Remove link", 40 | 41 | justifyLeft: "Align Left", 42 | justifyCenter: "Align Center", 43 | justifyRight: "Align Right", 44 | justifyFull: "Align Justify", 45 | 46 | horizontalRule: "Insert horizontal rule", 47 | 48 | fullscreen: "fullscreen", 49 | 50 | close: "Close", 51 | 52 | submit: "Confirm", 53 | reset: "Cancel", 54 | 55 | invalidUrl: "Invalid URL", 56 | required: "Required", 57 | description: "Description", 58 | title: "Title", 59 | text: "Text" 60 | } 61 | }, 62 | 63 | // User default options 64 | opts: {}, 65 | 66 | btnsGrps: { 67 | design: ['bold', 'italic', 'underline', 'strikethrough'], 68 | semantic: ['strong', 'em', 'del'], 69 | justify: ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'], 70 | lists: ['unorderedList', 'orderedList'] 71 | } 72 | }; 73 | 74 | 75 | 76 | (function(window, document, $, undefined){ 77 | 'use strict'; 78 | 79 | // @param : o are options 80 | // @param : p are params 81 | $.fn.trumbowyg = function(o, p){ 82 | if(o === Object(o) || !o){ 83 | return this.each(function(){ 84 | if(!$(this).data('trumbowyg')) 85 | $(this).data('trumbowyg', new Trumbowyg(this, o)); 86 | }); 87 | } else if(this.length === 1){ 88 | try { 89 | var t = $(this).data('trumbowyg'); 90 | switch(o){ 91 | // Modal box 92 | case 'openModal': 93 | return t.openModal(p.title, p.content); 94 | case 'closeModal': 95 | return t.closeModal(); 96 | case 'openModalInsert': 97 | return t.openModalInsert(p.title, p.fields, p.callback); 98 | 99 | // Selection 100 | case 'saveSelection': 101 | return t.saveSelection(); 102 | case 'getSelection': 103 | return t.selection; 104 | case 'getSelectedText': 105 | return t.selection+''; 106 | case 'restoreSelection': 107 | return t.restoreSelection(); 108 | 109 | // Destroy 110 | case 'destroy': 111 | return t.destroy(); 112 | 113 | // Empty 114 | case 'empty': 115 | return t.empty(); 116 | 117 | // Public options 118 | case 'lang': 119 | return t.lang; 120 | case 'duration': 121 | return t.o.duration; 122 | 123 | // HTML 124 | case 'html': 125 | return t.html(p); 126 | } 127 | } catch(e){} 128 | } 129 | 130 | return false; 131 | }; 132 | 133 | var Trumbowyg = function(editorElem, opts){ 134 | var t = this; 135 | // Get the document of the element. It use to makes the plugin 136 | // compatible on iframes. 137 | t.doc = editorElem.ownerDocument || document; 138 | // jQuery object of the editor 139 | t.$e = $(editorElem); 140 | t.$creator = $(editorElem); 141 | 142 | // Extend with options 143 | opts = $.extend(true, {}, opts, $.trumbowyg.opts); 144 | 145 | // Localization management 146 | if(typeof opts.lang === 'undefined' || typeof $.trumbowyg.langs[opts.lang] === 'undefined') 147 | t.lang = $.trumbowyg.langs.en; 148 | else 149 | t.lang = $.extend(true, {}, $.trumbowyg.langs.en, $.trumbowyg.langs[opts.lang]); 150 | 151 | // Defaults Options 152 | t.o = $.extend(true, {}, { 153 | lang: 'en', 154 | dir: 'ltr', 155 | duration: 200, // Duration of modal box animations 156 | 157 | mobile: false, 158 | tablet: true, 159 | closable: false, 160 | fullscreenable: true, 161 | fixedBtnPane: false, 162 | fixedFullWidth: false, 163 | autogrow: false, 164 | 165 | prefix: 'trumbowyg-', 166 | 167 | // WYSIWYG only 168 | convertLink: true, // TODO 169 | semantic: false, 170 | resetCss: false, 171 | 172 | btns: [ 173 | 'viewHTML', 174 | '|', 'formatting', 175 | '|', $.trumbowyg.btnsGrps.design, 176 | '|', 'link', 177 | '|', 'insertImage', 178 | '|', $.trumbowyg.btnsGrps.justify, 179 | '|', $.trumbowyg.btnsGrps.lists, 180 | '|', 'horizontalRule' 181 | ], 182 | btnsAdd: [], 183 | 184 | /** 185 | * When the button is associated to a empty object 186 | * func and title attributs are defined from the button key value 187 | * 188 | * For example 189 | * foo: {} 190 | * is equivalent to : 191 | * foo: { 192 | * func: 'foo', 193 | * title: this.lang.foo 194 | * } 195 | */ 196 | btnsDef: { 197 | viewHTML: { 198 | func: 'toggle' 199 | }, 200 | 201 | p: { 202 | func: 'formatBlock' 203 | }, 204 | blockquote: { 205 | func: 'formatBlock' 206 | }, 207 | h1: { 208 | func: 'formatBlock', 209 | title: t.lang.header + ' 1' 210 | }, 211 | h2: { 212 | func: 'formatBlock', 213 | title: t.lang.header + ' 2' 214 | }, 215 | h3: { 216 | func: 'formatBlock', 217 | title: t.lang.header + ' 3' 218 | }, 219 | h4: { 220 | func: 'formatBlock', 221 | title: t.lang.header + ' 4' 222 | }, 223 | 224 | bold: {}, 225 | italic: {}, 226 | underline: {}, 227 | strikethrough: {}, 228 | 229 | strong: { 230 | func: 'bold' 231 | }, 232 | em: { 233 | func: 'italic' 234 | }, 235 | del: { 236 | func: 'strikethrough' 237 | }, 238 | 239 | createLink: {}, 240 | unlink: {}, 241 | 242 | insertImage: {}, 243 | 244 | justifyLeft: {}, 245 | justifyCenter: {}, 246 | justifyRight: {}, 247 | justifyFull: {}, 248 | 249 | unorderedList: { 250 | func: 'insertUnorderedList' 251 | }, 252 | orderedList: { 253 | func: 'insertOrderedList' 254 | }, 255 | 256 | horizontalRule: { 257 | func: 'insertHorizontalRule' 258 | }, 259 | 260 | // Dropdowns 261 | formatting: { 262 | dropdown: ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4'] 263 | }, 264 | link: { 265 | dropdown: ['createLink', 'unlink'] 266 | } 267 | } 268 | }, opts); 269 | 270 | if(t.o.semantic && !opts.btns) 271 | t.o.btns = [ 272 | 'viewHTML', 273 | '|', 'formatting', 274 | '|', $.trumbowyg.btnsGrps.semantic, 275 | '|', 'link', 276 | '|', 'insertImage', 277 | '|', $.trumbowyg.btnsGrps.justify, 278 | '|', $.trumbowyg.btnsGrps.lists, 279 | '|', 'horizontalRule' 280 | ]; 281 | else if(opts && opts.btns) 282 | t.o.btns = opts.btns; 283 | 284 | t.init(); 285 | }; 286 | 287 | Trumbowyg.prototype = { 288 | init: function(){ 289 | var t = this; 290 | t.height = t.$e.css('height'); 291 | 292 | if(t.isEnabled()){ 293 | t.buildEditor(true); 294 | return; 295 | } 296 | 297 | t.buildEditor(); 298 | t.buildBtnPane(); 299 | 300 | t.fixedBtnPaneEvents(); 301 | 302 | t.buildOverlay(); 303 | }, 304 | 305 | buildEditor: function(disable){ 306 | var t = this, 307 | pfx = t.o.prefix, 308 | html = ''; 309 | 310 | 311 | if(disable === true){ 312 | if(!t.$e.is('textarea')){ 313 | var textarea = t.buildTextarea().val(t.$e.val()); 314 | t.$e.hide().after(textarea); 315 | } 316 | return; 317 | } 318 | 319 | 320 | t.$box = $('
', { 321 | class: pfx + 'box ' + pfx + t.o.lang + ' trumbowyg' 322 | }); 323 | 324 | t.isTextarea = true; 325 | if(t.$e.is('textarea')) 326 | t.$editor = $('
'); 327 | else { 328 | t.$editor = t.$e; 329 | t.$e = t.buildTextarea().val(t.$e.val()); 330 | t.isTextarea = false; 331 | } 332 | 333 | if(t.$creator.is('[placeholder]')) 334 | t.$editor.attr('placeholder', t.$creator.attr('placeholder')); 335 | 336 | t.$e.hide() 337 | .addClass(pfx + 'textarea'); 338 | 339 | 340 | if(t.isTextarea){ 341 | html = t.$e.val(); 342 | t.$box.insertAfter(t.$e) 343 | .append(t.$editor) 344 | .append(t.$e); 345 | } else { 346 | html = t.$editor.html(); 347 | t.$box.insertAfter(t.$editor) 348 | .append(t.$e) 349 | .append(t.$editor); 350 | t.syncCode(); 351 | } 352 | 353 | t.$editor.addClass(pfx + 'editor') 354 | .attr('contenteditable', true) 355 | .attr('dir', t.lang._dir || t.o.dir) 356 | .html(html); 357 | 358 | if(t.o.resetCss) 359 | t.$editor.addClass(pfx + 'reset-css'); 360 | 361 | if(!t.o.autogrow){ 362 | $.each([t.$editor, t.$e], function(i, $el){ 363 | $el.css({ 364 | height: t.height, 365 | overflow: 'auto' 366 | }); 367 | }); 368 | } 369 | 370 | if(t.o.semantic){ 371 | t.$editor.html( 372 | t.$editor.html() 373 | .replace('
', '

') 374 | .replace(' ', '') 375 | ); 376 | t.semanticCode(); 377 | } 378 | 379 | 380 | 381 | t.$editor 382 | .on('dblclick', 'img', function(e){ 383 | var $img = $(this); 384 | t.openModalInsert(t.lang.insertImage, { 385 | url: { 386 | label: 'URL', 387 | value: $img.attr('src'), 388 | required: true 389 | }, 390 | alt: { 391 | label: 'description', 392 | value: $img.attr('alt') 393 | } 394 | }, function(v){ 395 | $img.attr({ 396 | src: v.url, 397 | alt: v.alt 398 | }); 399 | }); 400 | e.stopPropagation(); 401 | }) 402 | .on('keyup', function(e){ 403 | t.semanticCode(false, e.which === 13); 404 | }) 405 | .on('focus', function(){ 406 | t.$creator.trigger('tbwfocus'); 407 | }) 408 | .on('blur', function(){ 409 | t.syncCode(); 410 | t.$creator.trigger('tbwblur'); 411 | }); 412 | }, 413 | 414 | 415 | // Build the Textarea which contain HTML generated code 416 | buildTextarea: function(){ 417 | return $('