puppet-courseware-manager 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.txt +8 -0
- data/README.txt +6 -5
- data/bin/courseware +6 -2
- data/lib/courseware/composer.rb +44 -52
- data/lib/courseware/dummy.rb +0 -1
- data/lib/courseware/help.rb +6 -7
- data/lib/courseware/manager.rb +4 -3
- data/lib/courseware/manager/validators.rb +70 -22
- data/lib/courseware/utils.rb +15 -29
- data/lib/courseware/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36a9e74fc4e32ff8eba79b73b7fda4eee3e6b6d8
|
4
|
+
data.tar.gz: 6896d7dad5206c44fdef7e251e0f35b132f2a2e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f088969a94f5d90a9a9954bc301731a51f0d40e8e05493a3964cedb61ed67730bb0da0ebd7557d0ab84073d64807eadf6ce84bee4bf902c05d9a6cc1fb493c23
|
7
|
+
data.tar.gz: e75e95efd177e911c26288e386b0fbaf532dd0cfa497c5436778916fdbac88d1125d9e77c1d99f158c78ce353e75cc7d6285c46a92562d2e558847517da38e69
|
data/CHANGELOG.txt
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
# Courseware Manager
|
2
2
|
## Release Notes
|
3
3
|
|
4
|
+
### v0.6.0
|
5
|
+
* Can now work with topic directories.
|
6
|
+
* Knows how to identify obsolete slides and images across all courses.
|
7
|
+
* Smarter use of `showoff info` to find which files are in use.
|
8
|
+
* Can package course variants containing only the files & images in use.
|
9
|
+
* Default to `no` when making irreversible changes to the repo.
|
10
|
+
|
11
|
+
|
4
12
|
### v0.5.4
|
5
13
|
* When running non-interactively and a default is provided, don't prompt for input.
|
6
14
|
|
data/README.txt
CHANGED
@@ -42,14 +42,15 @@ DESCRIPTION
|
|
42
42
|
Runs validation checks on the presentation. Defaults to running all the checks.
|
43
43
|
|
44
44
|
Validators:
|
45
|
-
obsolete: Lists all unreferenced images and slides. This reference checks
|
46
|
-
|
45
|
+
obsolete: Lists all unreferenced images and slides. This reference checks all
|
46
|
+
slides and all CSS stylesheets. This validation is case sensitive
|
47
|
+
and should be run from the toplevel courseware root directory.
|
47
48
|
|
48
|
-
missing: Lists all slides that are missing.
|
49
|
-
|
49
|
+
missing: Lists all slides and images that are missing. This validation is case
|
50
|
+
sensitive and should be run from within an individual course directory.
|
50
51
|
|
51
52
|
lint: Runs a markdown linter on each slide file, using our own style
|
52
|
-
definition.
|
53
|
+
definition. This should be run within a course directory.
|
53
54
|
|
54
55
|
|
55
56
|
* release [type]
|
data/bin/courseware
CHANGED
@@ -86,10 +86,14 @@ config[:pdf][:license] ||= nil
|
|
86
86
|
config[:release] ||= {}
|
87
87
|
config[:release][:links] ||= []
|
88
88
|
|
89
|
-
# Validating obsolete slides is special because we have to load all
|
89
|
+
# Validating obsolete slides is special because we have to load all courses;
|
90
90
|
# nonetheless, we do need *something* valid there, so drop in a dummy value.
|
91
91
|
if ARGV == ['validate', 'obsolete']
|
92
|
+
config[:presfile] ||= :none
|
93
|
+
|
94
|
+
elsif ARGV.first == 'compose'
|
92
95
|
config[:presfile] ||= 'showoff.json'
|
96
|
+
|
93
97
|
else
|
94
98
|
config[:presfile] ||= Courseware.choose_variant
|
95
99
|
end
|
@@ -153,7 +157,7 @@ begin
|
|
153
157
|
courseware.compose subject.first
|
154
158
|
|
155
159
|
when :package
|
156
|
-
courseware.package
|
160
|
+
courseware.package config[:presfile]
|
157
161
|
|
158
162
|
when :shell
|
159
163
|
courseware.debug
|
data/lib/courseware/composer.rb
CHANGED
@@ -3,11 +3,12 @@ class Courseware::Composer
|
|
3
3
|
@config = config
|
4
4
|
@repository = repository || Courseware::Repository.new(config)
|
5
5
|
|
6
|
+
return if @config[:presfile] == :none
|
7
|
+
|
6
8
|
if File.exists?(@config[:presfile])
|
7
9
|
@showoff = JSON.parse(File.read(@config[:presfile]))
|
8
10
|
@coursename = @showoff['name']
|
9
11
|
@prefix = @showoff['name'].gsub(' ', '_')
|
10
|
-
@sections = @showoff['sections']
|
11
12
|
end
|
12
13
|
end
|
13
14
|
|
@@ -16,31 +17,27 @@ class Courseware::Composer
|
|
16
17
|
|
17
18
|
if subject.nil?
|
18
19
|
display_tags
|
19
|
-
raise
|
20
|
+
raise 'Please re-run this task with a list of tags to include.'
|
20
21
|
end
|
22
|
+
raise 'Please re-run this task with a list of tags to include.' unless subject.is_a? Array
|
23
|
+
raise 'No master section defined in `showoff.json`' unless @showoff.include? 'master'
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
raise 'All sections must be represented as Hashes to customize.' unless section.is_a? Hash
|
26
|
-
|
27
|
-
if section.include? key
|
28
|
-
raise "A section in showoff.json refers to #{section[key]}, which does not exist." unless File.exist? section[key]
|
29
|
-
section['include'] = section[key]
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
25
|
+
newsections = {}
|
26
|
+
@showoff['master'].each do |section|
|
27
|
+
next if (section['tags'] & subject).empty?
|
33
28
|
|
34
|
-
|
35
|
-
|
36
|
-
section.select! { |k,v| k == 'include' }
|
29
|
+
name = section['name']
|
30
|
+
newsections[name] = section['content']
|
37
31
|
end
|
38
|
-
@showoff['sections'].reject! { |section| section.empty? }
|
39
32
|
|
40
33
|
name = Courseware.question('What would you like to call this variant?', 'custom').gsub(/\W+/, '_')
|
41
34
|
desc = Courseware.question("Enter a customized description if you'd like:")
|
42
35
|
|
36
|
+
@showoff.delete 'tags'
|
37
|
+
@showoff.delete 'master'
|
38
|
+
@showoff['name'] = name if name
|
43
39
|
@showoff['description'] = desc if desc
|
40
|
+
@showoff['sections'] = newsections
|
44
41
|
|
45
42
|
File.write("#{name}.json", JSON.pretty_generate(@showoff))
|
46
43
|
puts "Run your presentation with `showoff serve -f #{name}.json` or `rake present`"
|
@@ -51,61 +48,56 @@ class Courseware::Composer
|
|
51
48
|
on_release!
|
52
49
|
pristine!
|
53
50
|
|
54
|
-
|
55
|
-
subject = subject.to_s
|
56
|
-
content = Courseware.parse_showoff(subject)
|
51
|
+
content = JSON.parse(`showoff info -jf #{subject}`)
|
57
52
|
variant = File.basename(subject, '.json')
|
58
53
|
current = @repository.current(@coursename)
|
59
54
|
|
60
55
|
if variant == 'showoff'
|
61
|
-
variant = @prefix
|
62
56
|
output = @prefix
|
63
57
|
else
|
64
58
|
output = "#{@prefix}-#{variant}"
|
65
59
|
end
|
66
60
|
|
67
|
-
FileUtils.rm_rf "build/#{
|
68
|
-
FileUtils.mkdir_p "build/#{
|
69
|
-
FileUtils.cp subject, "build/#{
|
70
|
-
|
71
|
-
content['sections'].each do |section|
|
72
|
-
if section.is_a? Hash
|
73
|
-
path = section['include']
|
74
|
-
else
|
75
|
-
path = section
|
76
|
-
end
|
61
|
+
FileUtils.rm_rf "build/#{@prefix}"
|
62
|
+
FileUtils.mkdir_p "build/#{@prefix}"
|
63
|
+
FileUtils.cp subject, "build/#{@prefix}/showoff.json"
|
77
64
|
|
78
|
-
|
79
|
-
|
80
|
-
FileUtils.
|
81
|
-
|
82
|
-
|
65
|
+
# Copy in only the slides used
|
66
|
+
content['files'].each do |path|
|
67
|
+
FileUtils.mkdir_p "build/#{@prefix}/#{File.dirname(path)}"
|
68
|
+
FileUtils.cp path, "build/#{@prefix}/#{path}"
|
69
|
+
end
|
83
70
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
71
|
+
# Copy in only the images used
|
72
|
+
content['images'].each do |path|
|
73
|
+
FileUtils.mkdir_p "build/#{@prefix}/#{File.dirname(path)}"
|
74
|
+
FileUtils.cp path, "build/#{@prefix}/#{path}"
|
88
75
|
end
|
89
76
|
|
90
77
|
# support is special
|
91
|
-
FileUtils.cp_r '../_support', "build/#{
|
92
|
-
FileUtils.rm_f "build/#{
|
93
|
-
FileUtils.rm_f "build/#{
|
94
|
-
FileUtils.rm_f "build/#{
|
78
|
+
FileUtils.cp_r '../_support', "build/#{@prefix}/_support"
|
79
|
+
FileUtils.rm_f "build/#{@prefix}/_support/*.pem"
|
80
|
+
FileUtils.rm_f "build/#{@prefix}/_support/*.pub"
|
81
|
+
FileUtils.rm_f "build/#{@prefix}/_support/aws_credentials"
|
95
82
|
|
96
83
|
# duplicate from cwd to build/variant everything not already copied
|
97
|
-
Dir.glob('
|
98
|
-
next if
|
99
|
-
next if
|
100
|
-
next if
|
101
|
-
|
84
|
+
Dir.glob('**{,/*/**}/*').each do |path|
|
85
|
+
next if path.start_with? 'build'
|
86
|
+
next if path.start_with? 'stats'
|
87
|
+
next if path.start_with? 'pdf'
|
88
|
+
next if path.start_with? '_images'
|
89
|
+
next if path == 'user.js'
|
90
|
+
next if path =~ /^\w+.json$/
|
91
|
+
next if File.exist? "build/#{@prefix}/#{path}"
|
92
|
+
|
93
|
+
FileUtils.mkdir_p "build/#{@prefix}/#{File.dirname(path)}" unless File.directory?(path)
|
94
|
+
FileUtils.ln_s File.expand_path(path), "build/#{@prefix}/#{path}"
|
102
95
|
end
|
103
96
|
|
104
|
-
system("tar -C build --dereference -czf build/#{output}-#{current}.tar.gz #{
|
97
|
+
system("tar -C build --dereference -czf build/#{output}-#{current}.tar.gz #{@prefix}")
|
105
98
|
if Courseware.confirm("Would you like to clean up the output build directory?")
|
106
|
-
FileUtils.rm_rf "build/#{
|
99
|
+
FileUtils.rm_rf "build/#{@prefix}"
|
107
100
|
end
|
108
|
-
|
109
101
|
end
|
110
102
|
|
111
103
|
private
|
data/lib/courseware/dummy.rb
CHANGED
@@ -7,7 +7,6 @@ class Courseware::Dummy
|
|
7
7
|
showoff = Courseware.parse_showoff(@config[:presfile])
|
8
8
|
@coursename = showoff['name']
|
9
9
|
@prefix = showoff['name'].gsub(' ', '_')
|
10
|
-
@sections = showoff['sections']
|
11
10
|
@password = showoff['key']
|
12
11
|
@current = showoff['courseware_release']
|
13
12
|
end
|
data/lib/courseware/help.rb
CHANGED
@@ -46,16 +46,15 @@ class Courseware
|
|
46
46
|
Runs validation checks on the presentation. Defaults to running all the checks.
|
47
47
|
|
48
48
|
Validators:
|
49
|
-
obsolete: Lists all unreferenced images and slides. This reference checks
|
50
|
-
|
49
|
+
obsolete: Lists all unreferenced images and slides. This reference checks all
|
50
|
+
slides and all CSS stylesheets. This validation is case sensitive
|
51
|
+
and should be run from the toplevel courseware root directory.
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
missing: Lists all slides that are missing. Note that this does not check
|
55
|
-
for missing image files yet. Case sensitive.
|
53
|
+
missing: Lists all slides and images that are missing. This validation is case
|
54
|
+
sensitive and should be run from within an individual course directory.
|
56
55
|
|
57
56
|
lint: Runs a markdown linter on each slide file, using our own style
|
58
|
-
definition.
|
57
|
+
definition. This should be run within a course directory.
|
59
58
|
|
60
59
|
* release [type]
|
61
60
|
Orchestrate a courseware release. Defaults to `point`.
|
data/lib/courseware/manager.rb
CHANGED
@@ -15,10 +15,11 @@ class Courseware::Manager
|
|
15
15
|
@warnings = 0
|
16
16
|
@errors = 0
|
17
17
|
|
18
|
+
return if @config[:presfile] == :none
|
19
|
+
|
18
20
|
showoff = Courseware.parse_showoff(@config[:presfile])
|
19
21
|
@coursename = showoff['name']
|
20
22
|
@prefix = showoff['name'].gsub(' ', '_')
|
21
|
-
@sections = showoff['sections']
|
22
23
|
@password = showoff['key']
|
23
24
|
end
|
24
25
|
|
@@ -55,13 +56,13 @@ class Courseware::Manager
|
|
55
56
|
version = Courseware.increment(@repository.current(@coursename), type)
|
56
57
|
Courseware.bailout?("Building a release for #{@coursename} version #{version}.")
|
57
58
|
|
58
|
-
raise "Release notes not updated for #{version}"
|
59
|
+
raise "Release notes not updated for #{version}" if Dir.glob('Release-Notes*').select { |path| Courseware.grep(version, path) }.empty?
|
59
60
|
|
60
61
|
Courseware.dialog('Last Repository Commit', @repository.last_commit)
|
61
62
|
Courseware.bailout?('Abort now if the commit message displayed is not what you expected.')
|
62
63
|
build_pdfs(version)
|
63
64
|
point_of_no_return
|
64
|
-
Courseware.bailout?('Please inspect the generated PDF files and abort if corrections must be made.') do
|
65
|
+
Courseware.bailout?('Please inspect the generated PDF files and abort if corrections must be made.', true) do
|
65
66
|
@repository.discard(@config[:stylesheet])
|
66
67
|
end
|
67
68
|
|
@@ -1,51 +1,99 @@
|
|
1
1
|
class Courseware::Manager
|
2
2
|
|
3
3
|
def obsolete
|
4
|
-
|
5
|
-
allsections = Dir.glob('*.json').collect do |variant|
|
6
|
-
Courseware.parse_showoff(variant)['sections'] rescue nil
|
7
|
-
end.flatten.uniq
|
4
|
+
toplevel!
|
8
5
|
|
9
|
-
|
10
|
-
Dir.glob('
|
11
|
-
|
12
|
-
|
13
|
-
next if system("grep #{file} *.css */*.md >/dev/null 2>&1")
|
6
|
+
allslides = Dir.glob('_content/**/*.md')
|
7
|
+
allimages = Dir.glob('_images/**/*').reject {|path| path.include? '_images/src' }
|
8
|
+
slides = []
|
9
|
+
images = []
|
14
10
|
|
15
|
-
|
16
|
-
|
11
|
+
Dir.glob('*').each do |path|
|
12
|
+
next if path == 'spec'
|
13
|
+
next if path.start_with? '_'
|
14
|
+
next unless File.directory? path
|
15
|
+
|
16
|
+
print "Validating #{path}."
|
17
|
+
|
18
|
+
# stuff presentation local slides & images into the collections of available content
|
19
|
+
allslides.concat Dir.glob("#{path}/**/*.md").reject {|file| file.include?('README.md') ||
|
20
|
+
file.include?('_notes') ||
|
21
|
+
File.dirname(file) == path }
|
22
|
+
|
23
|
+
allimages.concat Dir.glob("#{path}/_images/**/*").reject {|file| file.include?('README.md') ||
|
24
|
+
file.include?('src/') ||
|
25
|
+
File.directory?(file) }
|
26
|
+
|
27
|
+
Dir.chdir(path) do
|
28
|
+
Dir.glob('*.json').each do |filename|
|
29
|
+
# determine which slides and images are actually used
|
30
|
+
content = JSON.parse(`showoff info -jf #{filename}`)
|
31
|
+
lslides = content['files'].map do |slide|
|
32
|
+
slide.start_with?('_') ? slide.sub('_shared', '_content') : "#{path}/#{slide}"
|
33
|
+
end
|
34
|
+
limages = content['images'].map do |image|
|
35
|
+
image.start_with?('_images/shared') ? image.sub('_images/shared', '_images') : "#{path}/#{image}"
|
36
|
+
end
|
37
|
+
|
38
|
+
slides.concat(lslides)
|
39
|
+
images.concat(limages)
|
40
|
+
|
41
|
+
print '.'
|
42
|
+
end
|
43
|
+
puts
|
44
|
+
end
|
17
45
|
end
|
18
46
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
47
|
+
# remove the intersection, and what's left over is obsolete
|
48
|
+
obs_slides = (allslides - slides.uniq!)
|
49
|
+
obs_images = (allimages - images.uniq!)
|
50
|
+
|
51
|
+
puts "Obsolete slides:" unless obs_slides.empty?
|
52
|
+
obs_slides.each do |slide|
|
53
|
+
puts " * #{slide}"
|
54
|
+
@warnings += 1
|
55
|
+
end
|
25
56
|
|
26
|
-
|
57
|
+
puts "Obsolete images:" unless obs_images.empty?
|
58
|
+
obs_images.each do |image|
|
59
|
+
puts " * #{image}"
|
27
60
|
@warnings += 1
|
28
61
|
end
|
29
62
|
end
|
30
63
|
|
31
64
|
def missing
|
32
|
-
|
65
|
+
courselevel!
|
66
|
+
|
67
|
+
filename = @config[:presfile]
|
68
|
+
content = JSON.parse(`showoff info -jf #{filename}`)
|
69
|
+
sections = content['files']
|
70
|
+
images = content['images']
|
33
71
|
|
34
|
-
# This seems backwards, but we do it this way to get a case sensitive match
|
72
|
+
# This seems backwards, but we do it this way to get a case sensitive match on a case-insensitive-preserving filesystem
|
35
73
|
# http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob -- Baby jesus is crying.
|
36
74
|
Dir.glob("**{,/*/**}/*.md") do |file|
|
37
75
|
sections.delete(file)
|
38
76
|
end
|
39
|
-
|
77
|
+
Dir.glob("_images/**{,/*/**}/*") do |file|
|
78
|
+
images.delete(file)
|
79
|
+
end
|
40
80
|
|
41
|
-
puts "Missing slides:"
|
81
|
+
puts "Missing slides:" unless sections.empty?
|
42
82
|
sections.each do |slide|
|
43
83
|
puts " * #{slide}"
|
44
84
|
@errors += 1
|
45
85
|
end
|
86
|
+
|
87
|
+
puts "Missing images:" unless images.empty?
|
88
|
+
images.each do |slide|
|
89
|
+
puts " * #{slide}"
|
90
|
+
@errors += 1
|
91
|
+
end
|
46
92
|
end
|
47
93
|
|
48
94
|
def lint
|
95
|
+
courselevel!
|
96
|
+
|
49
97
|
puts "Checking Markdown style:"
|
50
98
|
style = File.join(@config[:cachedir], 'templates', 'markdown_style.rb')
|
51
99
|
style = File.exists?(style) ? style : 'all'
|
data/lib/courseware/utils.rb
CHANGED
@@ -35,9 +35,15 @@ class Courseware
|
|
35
35
|
answers.include? STDIN.gets.strip.downcase
|
36
36
|
end
|
37
37
|
|
38
|
-
def self.bailout?(message)
|
39
|
-
|
40
|
-
|
38
|
+
def self.bailout?(message, required=false)
|
39
|
+
if required
|
40
|
+
print "#{message} Continue? [y/N]: "
|
41
|
+
options = ['y', 'yes']
|
42
|
+
else
|
43
|
+
print "#{message} Continue? [Y/n]: "
|
44
|
+
options = [ 'y', 'yes', '']
|
45
|
+
end
|
46
|
+
unless options.include? STDIN.gets.strip.downcase
|
41
47
|
if block_given?
|
42
48
|
yield
|
43
49
|
end
|
@@ -83,10 +89,12 @@ class Courseware
|
|
83
89
|
|
84
90
|
def self.choose_variant
|
85
91
|
variants = Dir.glob('*.json')
|
86
|
-
|
87
|
-
|
92
|
+
return :none if variants.empty?
|
88
93
|
return 'showoff.json' if variants == ['showoff.json']
|
89
94
|
|
95
|
+
maxlen = variants.max { |x,y| x.size <=> y.size }.size - 4 # accomodate for the extension we're stripping
|
96
|
+
|
97
|
+
|
90
98
|
# Ensures that the default `showoff.json` is listed first
|
91
99
|
variants.unshift "showoff.json"
|
92
100
|
variants.uniq!
|
@@ -103,31 +111,9 @@ class Courseware
|
|
103
111
|
variants[idx]
|
104
112
|
end
|
105
113
|
|
106
|
-
# TODO:
|
114
|
+
# TODO: we could use some validation here
|
107
115
|
def self.parse_showoff(filename)
|
108
|
-
|
109
|
-
sections = showoff['sections'].map do |entry, section|
|
110
|
-
next entry if entry.is_a? String and section.nil?
|
111
|
-
|
112
|
-
if entry.is_a? Hash
|
113
|
-
file = entry['include']
|
114
|
-
else
|
115
|
-
file = section
|
116
|
-
end
|
117
|
-
|
118
|
-
unless file
|
119
|
-
puts "Malformed entry: #{entry.inspect} - #{section.inspect}"
|
120
|
-
next nil
|
121
|
-
end
|
122
|
-
|
123
|
-
path = File.dirname(file)
|
124
|
-
data = JSON.parse(File.read(file))
|
125
|
-
|
126
|
-
data.map { |source| "#{path}/#{source}" }
|
127
|
-
end.flatten.compact
|
128
|
-
showoff['sections'] = sections
|
129
|
-
|
130
|
-
return showoff
|
116
|
+
JSON.parse(File.read(filename))
|
131
117
|
end
|
132
118
|
|
133
119
|
def self.get_component(initial)
|
data/lib/courseware/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: puppet-courseware-manager
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Ford
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mdl
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.18.2
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.18.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: word_wrap
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|