linner-hc 1.0.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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/CHANGELOG +129 -0
- data/Gemfile +14 -0
- data/LICENSE +22 -0
- data/README.md +74 -0
- data/Rakefile +8 -0
- data/bin/linner +11 -0
- data/docs/commands.md +29 -0
- data/docs/config.md +192 -0
- data/lib/linner.rb +261 -0
- data/lib/linner/archive.rb +53 -0
- data/lib/linner/asset.rb +102 -0
- data/lib/linner/bundler.rb +85 -0
- data/lib/linner/cache.rb +19 -0
- data/lib/linner/command.rb +133 -0
- data/lib/linner/compressor.rb +17 -0
- data/lib/linner/environment.rb +76 -0
- data/lib/linner/helper.rb +39 -0
- data/lib/linner/notifier.rb +24 -0
- data/lib/linner/reactor.rb +87 -0
- data/lib/linner/sprite.rb +127 -0
- data/lib/linner/template.rb +77 -0
- data/lib/linner/templates/app/images/.gitkeep +0 -0
- data/lib/linner/templates/app/images/logo.png +0 -0
- data/lib/linner/templates/app/scripts/app.coffee +3 -0
- data/lib/linner/templates/app/styles/app.scss +1 -0
- data/lib/linner/templates/app/templates/welcome.hbs +1 -0
- data/lib/linner/templates/app/views/index.html +21 -0
- data/lib/linner/templates/bin/server +3 -0
- data/lib/linner/templates/config.yml +54 -0
- data/lib/linner/templates/public/.gitkeep +1 -0
- data/lib/linner/templates/test/.gitkeep +0 -0
- data/lib/linner/templates/vendor/.gitkeep +1 -0
- data/lib/linner/version.rb +3 -0
- data/lib/linner/wrapper.rb +39 -0
- data/linner.gemspec +35 -0
- data/linner.gemspec.bak +34 -0
- data/spec/fixtures/app.js +1 -0
- data/spec/fixtures/config.yml +30 -0
- data/spec/linner/asset_spec.rb +33 -0
- data/spec/linner/bundler_spec.rb +26 -0
- data/spec/linner/environment_spec.rb +22 -0
- data/spec/linner/helper_spec.rb +26 -0
- data/spec/linner/sprites_spec.rb +23 -0
- data/spec/linner/template_spec.rb +16 -0
- data/spec/linner/wrapper_spec.rb +20 -0
- data/spec/spec_helper.rb +11 -0
- data/vendor/config.default.yml +13 -0
- data/vendor/livereload.js +1055 -0
- data/vendor/require_definition.js +60 -0
- metadata +289 -0
    
        data/lib/linner/cache.rb
    ADDED
    
    | @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            module Linner
         | 
| 2 | 
            +
              class Cache < Hash
         | 
| 3 | 
            +
                def miss? ns, path
         | 
| 4 | 
            +
                  asset = Asset.new path
         | 
| 5 | 
            +
                  if self["#{ns}:#{path}"] and self["#{ns}:#{path}"].mtime == asset.mtime
         | 
| 6 | 
            +
                    false
         | 
| 7 | 
            +
                  else
         | 
| 8 | 
            +
                    self["#{ns}:#{path}"] = asset
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def expire_by paths
         | 
| 13 | 
            +
                  is_include_partial_styles = paths.any? do |path|
         | 
| 14 | 
            +
                    Asset.new(path).stylesheet? and File.basename(path).start_with? "_"
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                  self.reject! {|k, v| v.stylesheet?} if is_include_partial_styles
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,133 @@ | |
| 1 | 
            +
            require "thor"
         | 
| 2 | 
            +
            require "listen"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Linner
         | 
| 5 | 
            +
              class Command < Thor
         | 
| 6 | 
            +
                include Thor::Actions
         | 
| 7 | 
            +
                map "-v" => :version
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def self.source_root
         | 
| 10 | 
            +
                  File.dirname(__FILE__)
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                desc "version", "show version"
         | 
| 14 | 
            +
                def version
         | 
| 15 | 
            +
                  puts Linner::VERSION
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                desc "check", "check dependencies"
         | 
| 19 | 
            +
                method_option :environment,
         | 
| 20 | 
            +
                              type: :string,
         | 
| 21 | 
            +
                              default: "development",
         | 
| 22 | 
            +
                              aliases: "-e",
         | 
| 23 | 
            +
                              desc: "Watch the choosen environment"
         | 
| 24 | 
            +
                def check
         | 
| 25 | 
            +
                  env.merge_with_environment(options[:environment])
         | 
| 26 | 
            +
                  message = Linner::Bundler.new(env).check
         | 
| 27 | 
            +
                  puts (message.first ? "🍵 :" : "👻 :") + message.last
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                desc "install", "install dependencies"
         | 
| 31 | 
            +
                method_option :environment,
         | 
| 32 | 
            +
                              type: :string,
         | 
| 33 | 
            +
                              default: "development",
         | 
| 34 | 
            +
                              aliases: "-e",
         | 
| 35 | 
            +
                              desc: "Watch the choosen environment"
         | 
| 36 | 
            +
                def install
         | 
| 37 | 
            +
                  begin
         | 
| 38 | 
            +
                    env.merge_with_environment(options[:environment])
         | 
| 39 | 
            +
                    Linner::Bundler.new(env).perform
         | 
| 40 | 
            +
                  rescue
         | 
| 41 | 
            +
                    puts "👻 : Install failed!"
         | 
| 42 | 
            +
                    puts $!
         | 
| 43 | 
            +
                    return
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                  puts "🍵 : Perfect installed all bundles!"
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                desc "build", "build assets"
         | 
| 49 | 
            +
                method_option :strict,
         | 
| 50 | 
            +
                              type: :boolean,
         | 
| 51 | 
            +
                              default: false,
         | 
| 52 | 
            +
                              aliases: "-s",
         | 
| 53 | 
            +
                              desc: "Use strict mode to replace revisiton."
         | 
| 54 | 
            +
                method_option :environment,
         | 
| 55 | 
            +
                              type: :string,
         | 
| 56 | 
            +
                              default: "production",
         | 
| 57 | 
            +
                              aliases: "-e",
         | 
| 58 | 
            +
                              desc: "Build the choosen environment"
         | 
| 59 | 
            +
                def build
         | 
| 60 | 
            +
                  Linner.compile = true
         | 
| 61 | 
            +
                  Linner.strict = true if options[:strict]
         | 
| 62 | 
            +
                  clean
         | 
| 63 | 
            +
                  env.merge_with_environment(options[:environment])
         | 
| 64 | 
            +
                  Bundler.new(env).perform
         | 
| 65 | 
            +
                  perform
         | 
| 66 | 
            +
                rescue
         | 
| 67 | 
            +
                  Notifier.error $!
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                desc "watch", "watch assets"
         | 
| 71 | 
            +
                method_option :environment,
         | 
| 72 | 
            +
                              type: :string,
         | 
| 73 | 
            +
                              default: "development",
         | 
| 74 | 
            +
                              aliases: "-e",
         | 
| 75 | 
            +
                              desc: "Watch the choosen environment"
         | 
| 76 | 
            +
                def watch
         | 
| 77 | 
            +
                  clean
         | 
| 78 | 
            +
                  env.merge_with_environment(options[:environment])
         | 
| 79 | 
            +
                  Bundler.new(env).perform
         | 
| 80 | 
            +
                  perform
         | 
| 81 | 
            +
                  watch_for_env
         | 
| 82 | 
            +
                  watch_for_perform
         | 
| 83 | 
            +
                  watch_for_reload rescue nil
         | 
| 84 | 
            +
                  sleep
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                desc "clean", "clean assets"
         | 
| 88 | 
            +
                def clean
         | 
| 89 | 
            +
                  FileUtils.rm_rf Dir.glob("#{env.public_folder}/*")
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                desc "new", "create the skeleton of project"
         | 
| 93 | 
            +
                def new(name)
         | 
| 94 | 
            +
                  directory('templates', name)
         | 
| 95 | 
            +
                  chmod("#{name}/bin/server", 0755)
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
              private
         | 
| 99 | 
            +
                def env
         | 
| 100 | 
            +
                  Linner.env
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def perform
         | 
| 104 | 
            +
                  Notifier.profile { Linner.perform }
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                def watch_for_perform
         | 
| 108 | 
            +
                  Listen.to env.watched_paths do |modified, added, removed|
         | 
| 109 | 
            +
                    Linner.cache.expire_by(modified + added + removed)
         | 
| 110 | 
            +
                    perform
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                def watch_for_reload
         | 
| 115 | 
            +
                  reactor = Reactor.supervise_as(:reactor).actors.first
         | 
| 116 | 
            +
                  Listen.to env.public_folder, relative_path: true do |modified, added, removed|
         | 
| 117 | 
            +
                    reactor.reload_browser(modified + added + removed)
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                def watch_for_env
         | 
| 122 | 
            +
                  Listen.to Linner.root, filter: /(config\.yml|Linnerfile)$/ do |modified, added, removed|
         | 
| 123 | 
            +
                    Linner.env = Environment.new Linner.config_file
         | 
| 124 | 
            +
                    Bundler.new(env).perform
         | 
| 125 | 
            +
                  end
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                def exit!
         | 
| 129 | 
            +
                  Notifier.exit
         | 
| 130 | 
            +
                  Kernel::exit
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
              end
         | 
| 133 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            require "uglifier"
         | 
| 2 | 
            +
            require "cssminify"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Linner
         | 
| 5 | 
            +
              class Compressor
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def self.compress(asset)
         | 
| 8 | 
            +
                  if asset.javascript? or asset.template?
         | 
| 9 | 
            +
                    Uglifier.compile asset.content, comments: "none"
         | 
| 10 | 
            +
                  elsif asset.stylesheet?
         | 
| 11 | 
            +
                    CSSminify.new.compress asset.content
         | 
| 12 | 
            +
                  else
         | 
| 13 | 
            +
                    asset.content
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,76 @@ | |
| 1 | 
            +
            require "yaml"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Linner
         | 
| 4 | 
            +
              class Environment
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def initialize(path)
         | 
| 7 | 
            +
                  @env ||= (YAML::load(File.read path) || Hash.new)
         | 
| 8 | 
            +
                  merge_with_convension
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                %w(app test vendor public).each do |method|
         | 
| 12 | 
            +
                  define_method("#{method}_folder") do
         | 
| 13 | 
            +
                    @env["paths"][method]
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def paths
         | 
| 18 | 
            +
                  groups.map { |group| group["paths"] }.flatten.uniq
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def watched_paths
         | 
| 22 | 
            +
                  [app_folder, vendor_folder, test_folder].select do |path|
         | 
| 23 | 
            +
                    File.exist? path
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                %w(revision notification).each do |method|
         | 
| 28 | 
            +
                  define_method("#{method}") do
         | 
| 29 | 
            +
                    @env[method]
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def manifest
         | 
| 34 | 
            +
                  revision["manifest"]
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def bundles
         | 
| 38 | 
            +
                  @env["bundles"] || []
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def sprites
         | 
| 42 | 
            +
                  @env["sprites"] || {}
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def modules_ignored
         | 
| 46 | 
            +
                  Dir.glob(@env["modules"]["ignored"])
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def wrapper
         | 
| 50 | 
            +
                  @env["modules"]["wrapper"]
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def definition
         | 
| 54 | 
            +
                  File.join public_folder, @env["modules"]["definition"]
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def groups
         | 
| 58 | 
            +
                  @env["groups"].values
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def environments
         | 
| 62 | 
            +
                  @env["environments"] || {}
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def merge_with_environment(environment)
         | 
| 66 | 
            +
                  return @env unless picked = environments[environment]
         | 
| 67 | 
            +
                  @env = @env.rmerge!(picked)
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                private
         | 
| 71 | 
            +
                def merge_with_convension
         | 
| 72 | 
            +
                  convension = YAML::load File.read(File.join File.dirname(__FILE__), "../../vendor", "config.default.yml")
         | 
| 73 | 
            +
                  @env = convension.rmerge!(@env)
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
              end
         | 
| 76 | 
            +
            end
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            module Linner
         | 
| 2 | 
            +
              module HashRecursiveMerge
         | 
| 3 | 
            +
                def rmerge!(other_hash)
         | 
| 4 | 
            +
                  merge!(other_hash) do |key, oldval, newval|
         | 
| 5 | 
            +
                    oldval.class == self.class ? oldval.rmerge!(newval) : newval
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              module Order
         | 
| 11 | 
            +
                def order_by(ary)
         | 
| 12 | 
            +
                  ary << "..." if not ary.include? "..."
         | 
| 13 | 
            +
                  order_ary = ary.inject([[]]) do |a, x|
         | 
| 14 | 
            +
                    x != "..." ? a.last << x : a<< []; a
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                  order_by_direction(order_ary.first, :before)
         | 
| 17 | 
            +
                  order_by_direction(order_ary.last, :after)
         | 
| 18 | 
            +
                  self
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              private
         | 
| 22 | 
            +
                def order_by_direction(ary, direction)
         | 
| 23 | 
            +
                  ary = ary.reverse if direction == :before
         | 
| 24 | 
            +
                  ary.each do |f|
         | 
| 25 | 
            +
                    next unless i = self.index {|x| x =~ /#{f}/i}
         | 
| 26 | 
            +
                    item = self.delete_at i
         | 
| 27 | 
            +
                    direction == :before ? self.unshift(item) : self.push(item)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            class Hash
         | 
| 34 | 
            +
              include Linner::HashRecursiveMerge
         | 
| 35 | 
            +
            end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            class Array
         | 
| 38 | 
            +
              include Linner::Order
         | 
| 39 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            require "terminal-notifier"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Linner
         | 
| 4 | 
            +
              class Notifier
         | 
| 5 | 
            +
                class << self
         | 
| 6 | 
            +
                  def profile
         | 
| 7 | 
            +
                    time = Time.now
         | 
| 8 | 
            +
                    yield
         | 
| 9 | 
            +
                    puts "🍜 : Done in #{"%.3f" % (Time.now - time)}s."
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def error(message)
         | 
| 13 | 
            +
                    abort message = "👻 : #{message}!"
         | 
| 14 | 
            +
                    if Linner.env.notification && TerminalNotifier.available?
         | 
| 15 | 
            +
                      TerminalNotifier.notify message, :title => "Linner"
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def exit
         | 
| 20 | 
            +
                    puts "\r🍵 : Let's take a break!"
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,87 @@ | |
| 1 | 
            +
            require "reel"
         | 
| 2 | 
            +
            require "json"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Linner
         | 
| 5 | 
            +
              class Reactor < Reel::Server::HTTP
         | 
| 6 | 
            +
                include Celluloid
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                Celluloid.logger = nil
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                attr_accessor :clients
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def initialize(host = "127.0.0.1", port = 35729)
         | 
| 13 | 
            +
                  @clients = []
         | 
| 14 | 
            +
                  super(host, port, &method(:on_connection))
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def on_connection(connection)
         | 
| 18 | 
            +
                  while request = connection.request
         | 
| 19 | 
            +
                    if request.websocket?
         | 
| 20 | 
            +
                      connection.detach
         | 
| 21 | 
            +
                      route_websocket request.websocket
         | 
| 22 | 
            +
                      return
         | 
| 23 | 
            +
                    else
         | 
| 24 | 
            +
                      route_request connection, request
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def route_request(connection, request)
         | 
| 30 | 
            +
                  if request.url.start_with? "/livereload.js"
         | 
| 31 | 
            +
                    return connection.respond :ok, {"Content_Type" => 'application/ecmascript'}, File.read(File.join(File.dirname(__FILE__), "../../vendor", "livereload.js"))
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  path = File.join(Linner.environment.public_folder, request.url[1..-1])
         | 
| 35 | 
            +
                  if File.exist?(path)
         | 
| 36 | 
            +
                    content_type = case File.extname(path)
         | 
| 37 | 
            +
                    when '.css' then 'text/css'
         | 
| 38 | 
            +
                    when '.js' then 'application/ecmascript'
         | 
| 39 | 
            +
                    when '.gif' then 'image/gif'
         | 
| 40 | 
            +
                    when '.jpeg', '.jpg' then 'image/jpeg'
         | 
| 41 | 
            +
                    when '.png' then 'image/png'
         | 
| 42 | 
            +
                    else; 'text/plain'
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                    return connection.respond :ok, {"Content_Type" => content_type, "Content_Length" => File.size(path)}, File.read(path)
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  connection.respond :not_found, "Not found"
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def route_websocket(socket)
         | 
| 51 | 
            +
                  socket << JSON.generate({
         | 
| 52 | 
            +
                    :command    => 'hello',
         | 
| 53 | 
            +
                    :protocols  => ['http://livereload.com/protocols/official-7'],
         | 
| 54 | 
            +
                    :serverName => 'reel-livereload'
         | 
| 55 | 
            +
                  })
         | 
| 56 | 
            +
                  if socket.url == "/livereload"
         | 
| 57 | 
            +
                    @clients << Client.new(socket)
         | 
| 58 | 
            +
                  else
         | 
| 59 | 
            +
                    socket.close
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def reload_browser(paths = [])
         | 
| 64 | 
            +
                  paths.each do |path|
         | 
| 65 | 
            +
                    @clients.each {|c| c.notify_asset_change path }
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                class Client
         | 
| 70 | 
            +
                  include Celluloid
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def initialize(socket)
         | 
| 73 | 
            +
                    @socket = socket
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  def notify_asset_change(path)
         | 
| 77 | 
            +
                    data = {
         | 
| 78 | 
            +
                      :path     => path,
         | 
| 79 | 
            +
                      :command  => 'reload',
         | 
| 80 | 
            +
                      :liveCSS  => true
         | 
| 81 | 
            +
                    }
         | 
| 82 | 
            +
                    @socket << JSON.generate(data)
         | 
| 83 | 
            +
                  rescue
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
            end
         | 
| @@ -0,0 +1,127 @@ | |
| 1 | 
            +
            require "chunky_png"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Linner
         | 
| 4 | 
            +
              ImageProxy = Struct.new(:path, :image, :top, :left) do
         | 
| 5 | 
            +
                def width
         | 
| 6 | 
            +
                  image.width
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def height
         | 
| 10 | 
            +
                  image.height
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              class Sprite
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                attr_accessor :root, :images
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def initialize(images)
         | 
| 19 | 
            +
                  @images = images.sort do |a, b|
         | 
| 20 | 
            +
                    diff = [b.width, b.height].max <=> [a.width, a.height].max
         | 
| 21 | 
            +
                    diff = [b.width, b.height].min <=> [a.width, a.height].min if diff.zero?
         | 
| 22 | 
            +
                    diff = b.height <=> a.height if diff.zero?
         | 
| 23 | 
            +
                    diff = b.width <=> a.width if diff.zero?
         | 
| 24 | 
            +
                    diff
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  @root = { :x => 0, :y => 0, :w => @images.first.width, :h => @images.first.height}
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def pack!
         | 
| 31 | 
            +
                  @images.each do |image|
         | 
| 32 | 
            +
                    if block = find_block(@root, image)
         | 
| 33 | 
            +
                      place_image(block, image)
         | 
| 34 | 
            +
                      split_block(block, image)
         | 
| 35 | 
            +
                    else
         | 
| 36 | 
            +
                      @root = expand_root(@root, image)
         | 
| 37 | 
            +
                      redo
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                  self
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def generate_style(config, name)
         | 
| 44 | 
            +
                  selector = config["selector"] || ".icon-"
         | 
| 45 | 
            +
                  url = config['url'] || config['path']
         | 
| 46 | 
            +
                  @images.inject("") do |style, image|
         | 
| 47 | 
            +
                    logical_path = Asset.new(image.path).logical_path
         | 
| 48 | 
            +
                    selector_with_pseudo_class = logical_path.chomp(File.extname(logical_path))
         | 
| 49 | 
            +
                      .gsub("/", "-")
         | 
| 50 | 
            +
                      .gsub("_active", ".active")
         | 
| 51 | 
            +
                      .gsub("_hover", ":hover")
         | 
| 52 | 
            +
                      .gsub("_", "-")
         | 
| 53 | 
            +
                    style <<
         | 
| 54 | 
            +
            "#{selector}#{selector_with_pseudo_class} {
         | 
| 55 | 
            +
              width: #{image.width}px;
         | 
| 56 | 
            +
              height: #{image.height}px;
         | 
| 57 | 
            +
              background: url(#{File.join url, name}) -#{image.left}px -#{image.top}px no-repeat;
         | 
| 58 | 
            +
            }
         | 
| 59 | 
            +
            "
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                private
         | 
| 64 | 
            +
                def find_block(root, image)
         | 
| 65 | 
            +
                  if root[:used]
         | 
| 66 | 
            +
                    find_block(root[:right], image) || find_block(root[:down], image)
         | 
| 67 | 
            +
                  elsif (image.width <= root[:w]) && (image.height <= root[:h])
         | 
| 68 | 
            +
                    root
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def place_image(block, image)
         | 
| 73 | 
            +
                  image.top = block[:y]
         | 
| 74 | 
            +
                  image.left = block[:x]
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def split_block(block, image)
         | 
| 78 | 
            +
                  block[:used] = true
         | 
| 79 | 
            +
                  block[:down] = {:x => block[:x], :y => block[:y] + image.height, :w => block[:w], :h => block[:h] - image.height}
         | 
| 80 | 
            +
                  block[:right] = {:x => block[:x] + image.width, :y => block[:y], :w => block[:w] - image.width, :h => image.height}
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def expand_root(root, image)
         | 
| 84 | 
            +
                  can_expand_down  = (image.width <= root[:w])
         | 
| 85 | 
            +
                  can_expand_right = (image.height <= root[:h])
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  should_expand_down  = can_expand_down  && (root[:w] >= (root[:h] + image.height))
         | 
| 88 | 
            +
                  should_expand_right = can_expand_right && (root[:h] >= (root[:w] + image.width))
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  if should_expand_right
         | 
| 91 | 
            +
                    expand_right(root, image.width)
         | 
| 92 | 
            +
                  elsif should_expand_down
         | 
| 93 | 
            +
                    expand_down(root, image.height)
         | 
| 94 | 
            +
                  elsif can_expand_right
         | 
| 95 | 
            +
                    expand_right(root, image.width)
         | 
| 96 | 
            +
                  elsif can_expand_down
         | 
| 97 | 
            +
                    expand_down(root, image.height)
         | 
| 98 | 
            +
                  else
         | 
| 99 | 
            +
                    raise RuntimeError, "Crashed!"
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def expand_right(root, width)
         | 
| 104 | 
            +
                  Hash[
         | 
| 105 | 
            +
                    :used  => true,
         | 
| 106 | 
            +
                    :x     => 0,
         | 
| 107 | 
            +
                    :y     => 0,
         | 
| 108 | 
            +
                    :w     => root[:w] + width,
         | 
| 109 | 
            +
                    :h     => root[:h],
         | 
| 110 | 
            +
                    :down  => root,
         | 
| 111 | 
            +
                    :right => { :x => root[:w], :y => 0, :w => width, :h => root[:h] }
         | 
| 112 | 
            +
                  ]
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                def expand_down(root, height)
         | 
| 116 | 
            +
                  Hash[
         | 
| 117 | 
            +
                    :used  => true,
         | 
| 118 | 
            +
                    :x     => 0,
         | 
| 119 | 
            +
                    :y     => 0,
         | 
| 120 | 
            +
                    :w     => root[:w],
         | 
| 121 | 
            +
                    :h     => root[:h] + height,
         | 
| 122 | 
            +
                    :down  => { :x => 0, :y => root[:h], :w => root[:w], :h => height },
         | 
| 123 | 
            +
                    :right => root
         | 
| 124 | 
            +
                  ]
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
              end # Sprite
         | 
| 127 | 
            +
            end # Linner
         |