helium 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +5 -0
- data/LICENCE +339 -0
- data/Manifest.txt +35 -0
- data/README.txt +266 -0
- data/Rakefile +12 -0
- data/bin/he +126 -0
- data/lib/helium.rb +50 -0
- data/lib/helium/configurable.rb +26 -0
- data/lib/helium/deployer.rb +216 -0
- data/lib/helium/generator.rb +69 -0
- data/lib/helium/jake.rb +73 -0
- data/lib/helium/logger.rb +15 -0
- data/lib/helium/trie.rb +59 -0
- data/lib/helium/views/deploy.erb +13 -0
- data/lib/helium/views/edit.erb +11 -0
- data/lib/helium/views/index.erb +67 -0
- data/lib/helium/views/layout.erb +60 -0
- data/lib/helium/views/missing.erb +6 -0
- data/lib/helium/web.rb +119 -0
- data/lib/helium/web_helpers.rb +65 -0
- data/templates/packages.js.erb +126 -0
- data/templates/project/.gitignore +4 -0
- data/templates/project/Jakefile +3 -0
- data/templates/project/jake.yml.erb +23 -0
- data/templates/project/source/__name__.js.erb +14 -0
- data/templates/project/test/index.html.erb +29 -0
- data/templates/web/config.ru +11 -0
- data/templates/web/custom.js +37 -0
- data/templates/web/deploy.yml +8 -0
- data/templates/web/public/prettify.css +6 -0
- data/templates/web/public/prettify.js +23 -0
- data/templates/web/public/style.css +40 -0
- data/test/deploy.yml +8 -0
- data/test/index.html +50 -0
- data/test/test_helium.rb +21 -0
- metadata +160 -0
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
|
6
|
+
Hoe.spec 'helium' do |p|
|
7
|
+
p.developer('James Coglan', 'james.coglan@othermedia.com')
|
8
|
+
p.extra_deps = [['grit','>= 0'], ['jake','>= 1.0.1'], ['packr','>= 3.1'],
|
9
|
+
['oyster','>= 0.9.3'], ['sinatra','>= 0.9.4'], ['rack','>= 1.0']]
|
10
|
+
end
|
11
|
+
|
12
|
+
# vim: syntax=ruby
|
data/bin/he
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
$VERBOSE = nil
|
3
|
+
home = File.join(File.dirname(__FILE__), '..')
|
4
|
+
|
5
|
+
require 'fileutils'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'oyster'
|
8
|
+
require 'jake'
|
9
|
+
require 'rack'
|
10
|
+
|
11
|
+
require File.join(home, 'lib', 'helium')
|
12
|
+
|
13
|
+
spec = Oyster.spec do
|
14
|
+
name 'helium -- the Git-backed JavaScript package server'
|
15
|
+
author 'James Coglan <jcoglan@googlemail.com>'
|
16
|
+
|
17
|
+
description <<-EOS
|
18
|
+
Helium is a web application for deploying and serving versioned copies of
|
19
|
+
JavaScript libraries from Git repositories. It uses Jake to build libraries
|
20
|
+
and extract dependency data, generating a manifest for JS.Packages so that
|
21
|
+
client sites can load objects from Helium on demand.
|
22
|
+
|
23
|
+
The following command line tools are available; run each one with `--help`
|
24
|
+
for more information.
|
25
|
+
|
26
|
+
`he install` creates a copy of the web frontend that you can serve using
|
27
|
+
Rack and Passenger.
|
28
|
+
|
29
|
+
`he create` generates a stub JavaScript project with all the files required
|
30
|
+
for Helium to deploy it.
|
31
|
+
|
32
|
+
`he serve` starts a web server to serve static files from any directory,
|
33
|
+
useful for testing code that requires a domain name to function.
|
34
|
+
EOS
|
35
|
+
|
36
|
+
subcommand :install do
|
37
|
+
synopsis 'he install DIRECTORY'
|
38
|
+
|
39
|
+
description <<-EOS
|
40
|
+
Installs a copy of the Helium web application in the given directory.
|
41
|
+
Set up an Apache VHost with the DocumentRoot pointing to DIRECTORY/public
|
42
|
+
to serve the app through Phusion Passenger.
|
43
|
+
EOS
|
44
|
+
end
|
45
|
+
|
46
|
+
subcommand :create do
|
47
|
+
synopsis 'he create NAME'
|
48
|
+
|
49
|
+
description <<-EOS
|
50
|
+
Creates a new JavaScript project with the given name, containing a stub
|
51
|
+
library file, a jake.yml config file and support code for generating
|
52
|
+
local package listings and running tests.
|
53
|
+
EOS
|
54
|
+
end
|
55
|
+
|
56
|
+
subcommand :serve do
|
57
|
+
synopsis 'he serve DIRECTORY'
|
58
|
+
|
59
|
+
description <<-EOS
|
60
|
+
Starts a webserver on port 8000 that serves the given directory over HTTP.
|
61
|
+
Useful for testing pages that require Ajax or services with domain specific
|
62
|
+
API keys like Google Maps.
|
63
|
+
EOS
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
begin; options = spec.parse
|
68
|
+
rescue; exit
|
69
|
+
end
|
70
|
+
|
71
|
+
if opts = options[:install]
|
72
|
+
dir = opts[:unclaimed].first
|
73
|
+
if dir.nil?
|
74
|
+
puts "Installation directory required -- type `he install --help` for more info"
|
75
|
+
exit
|
76
|
+
end
|
77
|
+
|
78
|
+
dir = File.expand_path(dir)
|
79
|
+
d = File.basename(dir)
|
80
|
+
|
81
|
+
puts "\nInstalling Helium app in #{dir}..."
|
82
|
+
Helium.generate('web', dir)
|
83
|
+
|
84
|
+
puts "\n... done, now set up your webserver to serve the app:\n\n"
|
85
|
+
puts " * Point Apache at #{d}/public to serve using Passenger"
|
86
|
+
puts " * Run `rackup #{d}/config.ru` to run it locally\n\n"
|
87
|
+
|
88
|
+
elsif opts = options[:create]
|
89
|
+
name = opts[:unclaimed].first
|
90
|
+
if name.nil?
|
91
|
+
puts "Project name required -- type `he create --help` for more info"
|
92
|
+
exit
|
93
|
+
end
|
94
|
+
|
95
|
+
dir = File.expand_path(name)
|
96
|
+
d = File.basename(dir)
|
97
|
+
|
98
|
+
puts "\nGenerating JavaScript project in #{dir}..."
|
99
|
+
Helium.generate('project', dir, :name => name)
|
100
|
+
|
101
|
+
puts "\nBuilding project using Jake..."
|
102
|
+
jake_hook(:file_created) do |build, package, type, path|
|
103
|
+
puts "create #{d}#{ path.sub(dir, '') }"
|
104
|
+
end
|
105
|
+
Jake.build!(dir)
|
106
|
+
|
107
|
+
puts "\n... done, now your new JavaScript project is ready.\n\n"
|
108
|
+
puts " * Build your project by running `jake` in the root directory"
|
109
|
+
puts " * We've added generated files to your .gitignore"
|
110
|
+
puts " * Keep your dependencies up-to-date in jake.yml"
|
111
|
+
puts " * Point test/index.html at your Helium server and write some tests!\n\n"
|
112
|
+
|
113
|
+
elsif opts = options[:serve]
|
114
|
+
dir = File.expand_path(opts[:unclaimed].first || '.')
|
115
|
+
app = Rack::File.new(dir)
|
116
|
+
|
117
|
+
server = %w[thin mongrel webrick].map { |name|
|
118
|
+
begin; Rack::Handler.get(name)
|
119
|
+
rescue LoadError; nil
|
120
|
+
end
|
121
|
+
}.compact.first
|
122
|
+
|
123
|
+
puts "Serving contents of #{dir} on port 8000..."
|
124
|
+
server.run(app, :Port => 8000)
|
125
|
+
end
|
126
|
+
|
data/lib/helium.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'observer'
|
2
|
+
require 'erb'
|
3
|
+
require 'yaml'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'find'
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'grit'
|
9
|
+
require 'jake'
|
10
|
+
require 'packr'
|
11
|
+
require 'oyster'
|
12
|
+
|
13
|
+
module Helium
|
14
|
+
|
15
|
+
VERSION = '0.1.0'
|
16
|
+
|
17
|
+
ROOT = File.expand_path(File.dirname(__FILE__))
|
18
|
+
TEMPLATES = File.join(ROOT, '..', 'templates')
|
19
|
+
ERB_EXT = '.erb'
|
20
|
+
JS_CONFIG_TEMPLATE = File.join(TEMPLATES, 'packages.js.erb')
|
21
|
+
|
22
|
+
CONFIG_FILE = 'deploy.yml'
|
23
|
+
REPOS = 'repos'
|
24
|
+
STATIC = 'static'
|
25
|
+
PACKAGES = 'helium-src.js'
|
26
|
+
PACKAGES_MIN = 'helium.js'
|
27
|
+
WEB_ROOT = 'js'
|
28
|
+
|
29
|
+
GIT = '.git'
|
30
|
+
HEAD = 'HEAD'
|
31
|
+
|
32
|
+
JS_CLASS = 'js.class'
|
33
|
+
LOADER_FILE = 'loader.js'
|
34
|
+
JAKE_FILE = Jake::CONFIG_FILE
|
35
|
+
|
36
|
+
SEP = File::SEPARATOR
|
37
|
+
BYTE = 1024.0
|
38
|
+
|
39
|
+
ERB_TRIM_MODE = '-'
|
40
|
+
|
41
|
+
%w[trie configurable deployer generator logger].each do |file|
|
42
|
+
require File.join(ROOT, 'helium', file)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.generate(template, dir, options = {})
|
46
|
+
Generator.new(template, dir, options).run!
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Helium
|
2
|
+
module Configurable
|
3
|
+
|
4
|
+
def configure
|
5
|
+
yield(configuration)
|
6
|
+
end
|
7
|
+
|
8
|
+
def configuration
|
9
|
+
@configuration ||= Configuration.new
|
10
|
+
end
|
11
|
+
alias :config :configuration
|
12
|
+
|
13
|
+
class Configuration
|
14
|
+
def initialize(hash = {})
|
15
|
+
@options = {}.merge(hash)
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(name, value = nil)
|
19
|
+
@options[name] = value unless value.nil?
|
20
|
+
@options[name]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,216 @@
|
|
1
|
+
module Helium
|
2
|
+
# The +Deployer+ class is responsible for performing all of Helium's central functions:
|
3
|
+
# downloading projects from Git, exporting static copies of branches, building projects
|
4
|
+
# using Jake and generating the dependency file.
|
5
|
+
#
|
6
|
+
# To run, the class requires a file called `deploy.yml` in the target directory, listing
|
7
|
+
# projects with their Git URLs. (See tests and README for examples.)
|
8
|
+
#
|
9
|
+
class Deployer
|
10
|
+
include Observable
|
11
|
+
|
12
|
+
# Initialize using the directory containing the `deploy.yml` file, and the name of
|
13
|
+
# the directory to export repositories and static files to.
|
14
|
+
#
|
15
|
+
# deployer = Helium::Deployer.new('path/to/app', 'js')
|
16
|
+
#
|
17
|
+
def initialize(path, output_dir = 'output', options = {})
|
18
|
+
@path = File.expand_path(path)
|
19
|
+
@config = join(@path, CONFIG_FILE)
|
20
|
+
raise "Expected config file at #{@config}" unless File.file?(@config)
|
21
|
+
@output_dir = join(@path, output_dir)
|
22
|
+
@options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a hash of projects names and their Git URLs.
|
26
|
+
def projects
|
27
|
+
return @projects if defined?(@projects)
|
28
|
+
|
29
|
+
data = YAML.load(File.read(@config))
|
30
|
+
raise "No configuration for JS.Class" unless js_class = data[JS_CLASS]
|
31
|
+
@jsclass_version = js_class['version']
|
32
|
+
|
33
|
+
@projects = data['projects'] || {}
|
34
|
+
@projects[JS_CLASS] = js_class['repository']
|
35
|
+
@projects
|
36
|
+
end
|
37
|
+
|
38
|
+
# Runs all the deploy actions. If given a project name, only checks out and builds
|
39
|
+
# the given project, otherwise builds all projects in `deploy.yml`.
|
40
|
+
def run!(project = nil)
|
41
|
+
return deploy!(project) if project
|
42
|
+
projects.each { |project, url| deploy!(project, false) }
|
43
|
+
run_builds!
|
44
|
+
end
|
45
|
+
|
46
|
+
# Deploys an individual project by checking it out from Git, exporting static copies
|
47
|
+
# of all its branches and building them using Jake.
|
48
|
+
def deploy!(project, build = true)
|
49
|
+
checkout(project)
|
50
|
+
export(project)
|
51
|
+
run_builds! if build
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks out (or updates if already checked out) a project by name from its Git
|
55
|
+
# repository. If the project is new, we use `git clone` to copy it, otherwise
|
56
|
+
# we use `git fetch` to update it.
|
57
|
+
def checkout(project)
|
58
|
+
dir = repo_dir(project)
|
59
|
+
if File.directory?(join(dir, GIT))
|
60
|
+
log :git_fetch, "Updating Git repo in #{ dir }"
|
61
|
+
cd(dir) { `git fetch origin` }
|
62
|
+
else
|
63
|
+
url = projects[project]
|
64
|
+
log :git_clone, "Cloning Git repo #{ url } into #{ dir }"
|
65
|
+
`git clone #{ url } "#{ dir }"`
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Exports static copies of a project from every branch and tag in its Git repository.
|
70
|
+
# Existing static copies on disk are destroyed and replaced.
|
71
|
+
def export(project)
|
72
|
+
repo_dir = repo_dir(project)
|
73
|
+
repo = Grit::Repo.new(repo_dir)
|
74
|
+
branches = repo.remotes + repo.tags
|
75
|
+
|
76
|
+
mkdir_p(static_dir(project))
|
77
|
+
|
78
|
+
branches.each do |branch|
|
79
|
+
name, commit = branch.name.split(SEP).last, branch.commit.id
|
80
|
+
next if HEAD == name
|
81
|
+
|
82
|
+
target = static_dir(project, commit)
|
83
|
+
|
84
|
+
log :export, "Exporting branch '#{ name }' of '#{ project }' into #{ target }"
|
85
|
+
rm_rf(target) if File.directory?(target)
|
86
|
+
cp_r(repo_dir, target)
|
87
|
+
|
88
|
+
cd(target) {
|
89
|
+
if repo.branches.map { |b| b.name }.include?(name)
|
90
|
+
`git checkout #{ name }`
|
91
|
+
`git merge #{ branch.name }`
|
92
|
+
else
|
93
|
+
`git checkout -b #{ name } #{ branch.name }`
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Scans all the checked-out projects for Jake build files and builds those projects
|
100
|
+
# that have such a file. As the build progresses we use Jake event hooks to collect
|
101
|
+
# dependencies and generated file paths, and when all builds are finished we generate
|
102
|
+
# a JS.Packages file listing all the files discovered. This file should be included
|
103
|
+
# in web pages to set up the the packages manager for loading our projects.
|
104
|
+
def run_builds!(options = nil)
|
105
|
+
options ||= @options
|
106
|
+
|
107
|
+
@tree = Trie.new
|
108
|
+
@custom = options[:custom]
|
109
|
+
@domain = options[:domain]
|
110
|
+
manifest = []
|
111
|
+
|
112
|
+
# Loop over checked-out projects. Skip directories with no Jake file.
|
113
|
+
Find.find(static_dir) do |path|
|
114
|
+
next unless File.directory?(path) and File.file?(join(path, JAKE_FILE))
|
115
|
+
|
116
|
+
project, commit = *path.split(SEP)[-2..-1]
|
117
|
+
branch = Grit::Repo.new(path).head.name
|
118
|
+
Jake.clear_hooks!
|
119
|
+
|
120
|
+
# Event listener to capture file information from Jake
|
121
|
+
hook = lambda do |build, package, build_type, file|
|
122
|
+
if build_type == :min
|
123
|
+
@js_loader = file if File.basename(file) == LOADER_FILE and
|
124
|
+
project == JS_CLASS and
|
125
|
+
branch == @jsclass_version
|
126
|
+
|
127
|
+
file = file.sub(path, '')
|
128
|
+
manifest << join(project, commit, file)
|
129
|
+
@tree[[project, branch]] = commit
|
130
|
+
@tree[[project, branch, file]] = package.meta
|
131
|
+
end
|
132
|
+
end
|
133
|
+
jake_hook(:file_created, &hook)
|
134
|
+
jake_hook(:file_not_changed, &hook)
|
135
|
+
|
136
|
+
log :jake_build, "Building branch '#{ branch }' of '#{ project }' from #{ join(path, JAKE_FILE) }"
|
137
|
+
|
138
|
+
begin; Jake.build!(path)
|
139
|
+
rescue; end
|
140
|
+
end
|
141
|
+
|
142
|
+
generate_manifest!
|
143
|
+
manifest + [PACKAGES, PACKAGES_MIN]
|
144
|
+
end
|
145
|
+
|
146
|
+
# Removes any repositories and static files for projects not listed in the the
|
147
|
+
# `deploy.yml` file.
|
148
|
+
def cleanup!
|
149
|
+
[repo_dir, static_dir].each do |dir|
|
150
|
+
next unless File.directory?(dir)
|
151
|
+
(Dir.entries(dir) - %w[. ..]).each do |entry|
|
152
|
+
path = join(dir, entry)
|
153
|
+
next unless File.directory?(path)
|
154
|
+
rm_rf(path) unless projects.has_key?(entry)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns the path to the Git repository for a given project.
|
160
|
+
def repo_dir(project = nil)
|
161
|
+
path = [@output_dir, REPOS, project].compact
|
162
|
+
join(*path)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns the path to the static export directory for a given project and branch.
|
166
|
+
def static_dir(project = nil, branch = nil)
|
167
|
+
path = [@output_dir, STATIC, project, branch].compact
|
168
|
+
join(*path)
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
# Generates JS.Packages dependency file from ERB template and compresses the result
|
174
|
+
def generate_manifest!
|
175
|
+
template = File.read(JS_CONFIG_TEMPLATE)
|
176
|
+
code = ERB.new(template, nil, ERB_TRIM_MODE).result(binding)
|
177
|
+
packed = Packr.pack(code, :shrink_vars => true)
|
178
|
+
|
179
|
+
mkdir_p(static_dir)
|
180
|
+
File.open(static_dir(PACKAGES), 'w') { |f| f.write(code) }
|
181
|
+
File.open(static_dir(PACKAGES_MIN), 'w') { |f| f.write(packed) }
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns +true+ iff the set of files contains any dependency data.
|
185
|
+
def has_manifest?(config)
|
186
|
+
case config
|
187
|
+
when Trie then config.any? { |path, conf| has_manifest?(conf) }
|
188
|
+
when Hash then config.has_key?(:provides)
|
189
|
+
else nil
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Notifies observers by sending a log message.
|
194
|
+
def log(*args)
|
195
|
+
changed(true)
|
196
|
+
notify_observers(*args)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Shorthand for <tt>File.join</tt>
|
200
|
+
def join(*args)
|
201
|
+
File.join(*args)
|
202
|
+
end
|
203
|
+
|
204
|
+
# We use +method_missing+ to create shorthands for +FileUtils+ methods.
|
205
|
+
def method_missing(*args, &block)
|
206
|
+
FileUtils.__send__(*args, &block)
|
207
|
+
end
|
208
|
+
|
209
|
+
def `(command)
|
210
|
+
puts command
|
211
|
+
system(command)
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Helium
|
2
|
+
# The +Generator+ class is used by the command-line tools to create copies of
|
3
|
+
# the web app and the JavaScript project template. It copies the contents of
|
4
|
+
# one of the +template+ directories into a local directory, expanding any files
|
5
|
+
# with .erb extensions as ERB templates.
|
6
|
+
#
|
7
|
+
# For example, a file <tt>jake.yml.erb</tt> will be copied into the target dir as
|
8
|
+
# <tt>jake.yml</tt> after being evaluated using ERB.
|
9
|
+
#
|
10
|
+
# If a filename contains variable names enclosed in double-underscores, the
|
11
|
+
# resulting copy will have those replaced by the value of the named instance
|
12
|
+
# variable. For example, <tt>__name__.js</tt> will be copied to <tt>myproj.js</tt>
|
13
|
+
# if <tt>@name = 'myproj'</tt>.
|
14
|
+
#
|
15
|
+
class Generator
|
16
|
+
|
17
|
+
# Generators are initialized using the name of the template (a collection of
|
18
|
+
# files in the +templates+ directory, a target directory and an option hash.
|
19
|
+
# Keys in the option hash become instance variables accessible to ERB templates.
|
20
|
+
def initialize(template, dir, options = {})
|
21
|
+
options.each do |key, value|
|
22
|
+
instance_variable_set("@#{key}", value)
|
23
|
+
end
|
24
|
+
@_source = join(TEMPLATES, template)
|
25
|
+
@_directory = expand_path(dir)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Runs the generator, copying all files as required. All ERB/name replacement
|
29
|
+
# is handled in this method.
|
30
|
+
def run!
|
31
|
+
Find.find(@_source) do |path|
|
32
|
+
next unless file?(path)
|
33
|
+
content = read(path)
|
34
|
+
|
35
|
+
# Replace variable names in file paths
|
36
|
+
path = path.sub(@_source, '').gsub(/__(.+?)__/) { instance_variable_get("@#{$1}") }
|
37
|
+
target = join(@_directory, path)
|
38
|
+
|
39
|
+
# Evaluate using ERB if required
|
40
|
+
if extname(path) == ERB_EXT
|
41
|
+
content = ERB.new(content).result(binding)
|
42
|
+
target = join(dirname(target), basename(target, ERB_EXT))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Generate destination file
|
46
|
+
FileUtils.mkdir_p(dirname(target))
|
47
|
+
open(target, 'w') { |f| f.write(content) }
|
48
|
+
puts "create #{ basename(@_directory) }#{ target.sub(@_directory, '') }"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Provide shorthand access to all +File+ methods.
|
53
|
+
def method_missing(*args, &block)
|
54
|
+
File.__send__(*args, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a camelcased copy of the string, for example:
|
58
|
+
#
|
59
|
+
# camelize('my-project')
|
60
|
+
# #=> 'MyProject'
|
61
|
+
#
|
62
|
+
def camelize(string)
|
63
|
+
string.gsub(/^(.)/) { $1.upcase }.
|
64
|
+
gsub(/[\s\-\_](.)/) { $1.upcase }
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|