helium 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|