ruby_extensions 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.
- data/init.rb +0 -0
- data/lib/m_array.rb +101 -0
- data/lib/m_class.rb +10 -0
- data/lib/m_dir.rb +25 -0
- data/lib/m_float.rb +5 -0
- data/lib/m_kernel.rb +12 -0
- data/lib/m_logger.rb +38 -0
- data/lib/m_math.rb +15 -0
- data/lib/m_object.rb +78 -0
- data/lib/m_string.rb +137 -0
- data/lib/ruby_extensions.rb +21 -0
- data/lib/tasks/rubyforge_config.yml +5 -0
- metadata +79 -0
data/init.rb
ADDED
|
File without changes
|
data/lib/m_array.rb
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
class Array
|
|
2
|
+
|
|
3
|
+
# This method is useful when you have a method that looks like this:
|
|
4
|
+
# def foo(*args)
|
|
5
|
+
# do something
|
|
6
|
+
# end
|
|
7
|
+
# The problem is when you use the * like that everything that comes in is
|
|
8
|
+
# an array. Here are a few problems with this:
|
|
9
|
+
# foo([1,2,3])
|
|
10
|
+
# When you pass an array into this type of method you get the following nested array:
|
|
11
|
+
# [[1,2,3]]
|
|
12
|
+
# The from_args method, if called, would do this:
|
|
13
|
+
# args.from_args # => [1,2,3]
|
|
14
|
+
# Now say you called this method like such:
|
|
15
|
+
# foo(1)
|
|
16
|
+
# args would be [1]
|
|
17
|
+
# args.from_args # => 1
|
|
18
|
+
# Finally
|
|
19
|
+
# foo
|
|
20
|
+
# args.from_args # => nil
|
|
21
|
+
def from_args
|
|
22
|
+
unless self.empty?
|
|
23
|
+
args = self#.flatten
|
|
24
|
+
case args.size
|
|
25
|
+
when 1
|
|
26
|
+
return args.first # if there was only one arg passed, return just that, without the array
|
|
27
|
+
when 0
|
|
28
|
+
return nil # if no args were passed return nil
|
|
29
|
+
else
|
|
30
|
+
return args # else return the array back, cause chances are that's what they passed you!
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# This allows you to delete an array of values from another array.
|
|
36
|
+
# [1,2,3,4,5].delete_from_array([2,3,5]) # => [1,4]
|
|
37
|
+
def delete_from_array(args)
|
|
38
|
+
self.collect{ |x| x unless args.include?(x)}.compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# This calls the delete_from_array method, but will permantly replace the existing array.
|
|
42
|
+
def delete_from_array!(args)
|
|
43
|
+
self.replace(delete_from_array(args))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# This will return a new instance of the array sorted randomly.
|
|
47
|
+
def randomize(&block)
|
|
48
|
+
if block_given?
|
|
49
|
+
self.sort {|x,y| yield x, y}
|
|
50
|
+
else
|
|
51
|
+
self.sort_by { rand }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# This calls the randomize method, but will permantly replace the existing array.
|
|
56
|
+
def randomize!(&block)
|
|
57
|
+
if block_given?
|
|
58
|
+
self.replace(self.randomize(&block))
|
|
59
|
+
else
|
|
60
|
+
self.replace(self.randomize)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# This will pick a random value from the array
|
|
65
|
+
def pick_random
|
|
66
|
+
self[rand(self.length)]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# This allows you to easily recurse of the array randomly.
|
|
70
|
+
def random_each
|
|
71
|
+
self.randomize.each {|x| yield x}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def subset?(other)
|
|
75
|
+
self.each do |x|
|
|
76
|
+
return false if !(other.include? x)
|
|
77
|
+
end
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def superset?(other)
|
|
82
|
+
other.subset?(self)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# This will give you a count, as a Hash, of all the values in the Array.
|
|
86
|
+
# %w{spam spam eggs ham eggs spam}.count # => {"eggs"=>2, "ham"=>1, "spam"=>3}
|
|
87
|
+
def count
|
|
88
|
+
k = Hash.new(0)
|
|
89
|
+
self.each{|x| k[x] += 1}
|
|
90
|
+
k
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# This will invert the index and the values and return a Hash of the results.
|
|
94
|
+
# %w{red yellow orange}.invert # => {"red"=>0, "orange"=>2, "yellow"=>1}
|
|
95
|
+
def invert
|
|
96
|
+
h = {}
|
|
97
|
+
self.each_with_index{|x,i| h[x] = i}
|
|
98
|
+
h
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
end
|
data/lib/m_class.rb
ADDED
data/lib/m_dir.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Dir
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
#creates multiple directories
|
|
5
|
+
def self.make_dirs(path)
|
|
6
|
+
dir_names = path.split('/')
|
|
7
|
+
new_path=""
|
|
8
|
+
dir_names.each do |x|
|
|
9
|
+
new_path = new_path << x << '/'
|
|
10
|
+
if !File.directory?(new_path)
|
|
11
|
+
Dir.mkdir(new_path)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
#if a directory exists, it deletes everything in it, otherwise it creates the directory
|
|
18
|
+
def self.prep_dir(path)
|
|
19
|
+
if File.directory?(path)
|
|
20
|
+
Dir.foreach(path) {|x| File.delete(path + x) unless File.directory?(path + x)}
|
|
21
|
+
else
|
|
22
|
+
Dir.make_dirs(path)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/m_float.rb
ADDED
data/lib/m_kernel.rb
ADDED
data/lib/m_logger.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class Logger
|
|
2
|
+
|
|
3
|
+
%W[error debug fatal info warn].each do |level|
|
|
4
|
+
class_eval %{
|
|
5
|
+
alias_method :old_#{level}, :#{level}
|
|
6
|
+
|
|
7
|
+
def #{level}(progname = nil, &block)
|
|
8
|
+
old_#{level}(format_progname(progname), &block)
|
|
9
|
+
end
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def format_message(severity, timestamp, progname, msg)
|
|
14
|
+
# Check for characters at the start of the message string indicating special formatting
|
|
15
|
+
# Make the typical case as efficient as possible
|
|
16
|
+
if not msg[0..1] == '$@'
|
|
17
|
+
return "[#{severity}]\t#{timestamp}\t#{msg}\n"
|
|
18
|
+
end
|
|
19
|
+
format = msg[2..2]
|
|
20
|
+
msg = msg[3..-1]
|
|
21
|
+
# Original format
|
|
22
|
+
if format == 'O'
|
|
23
|
+
return "#{timestamp}\t[#{severity}]\t#{msg}\n"
|
|
24
|
+
elsif format == 'S'
|
|
25
|
+
return "#{msg}\n"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
def format_progname(progname)
|
|
32
|
+
unless progname.nil?
|
|
33
|
+
progname = "#{progname.to_s}\n#{progname.backtrace.join("\n\t")}\n" if progname.is_a? Exception
|
|
34
|
+
end
|
|
35
|
+
progname
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
data/lib/m_math.rb
ADDED
data/lib/m_object.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
class Object
|
|
2
|
+
include ActionView::Helpers::TextHelper
|
|
3
|
+
include ActionView::Helpers::DateHelper
|
|
4
|
+
|
|
5
|
+
def running_time(message = "", logger = nil)
|
|
6
|
+
s_time = Time.now
|
|
7
|
+
s = "---Starting at #{s_time}---"
|
|
8
|
+
puts s
|
|
9
|
+
logger.info s unless logger.nil?
|
|
10
|
+
yield if block_given?
|
|
11
|
+
e_time = Time.now
|
|
12
|
+
e = "---Ending at #{e_time}---"
|
|
13
|
+
puts e
|
|
14
|
+
logger.info e unless logger.nil?
|
|
15
|
+
x = "Running time #{distance_of_time_in_words(s_time, e_time, true)} (#{e_time - s_time} seconds)."
|
|
16
|
+
x += " [MESSAGE]: #{message}" unless message.blank?
|
|
17
|
+
puts x
|
|
18
|
+
logger.info x unless logger.nil?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
=begin
|
|
22
|
+
ivar_cache allows you to cache the results of the block into an instance variable in a class,
|
|
23
|
+
and then will serve up that instance variable the next time you call that method again.
|
|
24
|
+
|
|
25
|
+
old way:
|
|
26
|
+
def show_page_link
|
|
27
|
+
unless @show_page_link # check if instance variable exists
|
|
28
|
+
# if the instance variable doesn't exist let's do some work and assign it to the instance variable.
|
|
29
|
+
@show_page_link = link_to("show", some_url(:id => self.id, :foo => bar, etc... => etc))
|
|
30
|
+
end
|
|
31
|
+
@show_page_link # now return the instance variable
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
new way:
|
|
35
|
+
def show_page_link
|
|
36
|
+
ivar_cache do
|
|
37
|
+
link_to("show", some_url(:id => self.id, :foo => bar, etc... => etc))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
this does everything the old way did, but it is much cleaner, and a lot less code!
|
|
42
|
+
in case you're wondering it caches the result, by default, to an instance variable named after the method,
|
|
43
|
+
so in our above example the instance variable would be name, @show_page_link. this can be overridden like such:
|
|
44
|
+
|
|
45
|
+
def show_page_link
|
|
46
|
+
ivar_cache("foo_var") do
|
|
47
|
+
link_to("show", some_url(:id => self.id, :foo => bar, etc... => etc))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
now it will cache it to @foo_var
|
|
52
|
+
=end
|
|
53
|
+
def ivar_cache(var_name = nil,&block)
|
|
54
|
+
if var_name.nil?
|
|
55
|
+
call = caller[0]
|
|
56
|
+
var_name = call[(call.index('`')+1)...call.index("'")]
|
|
57
|
+
end
|
|
58
|
+
var = instance_variable_get("@#{var_name}")
|
|
59
|
+
unless var
|
|
60
|
+
return instance_variable_set("@#{var_name}", yield) if block_given?
|
|
61
|
+
end
|
|
62
|
+
instance_variable_get("@#{var_name}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def namespaces
|
|
66
|
+
ivar_cache("object_namespaces") do
|
|
67
|
+
nss = []
|
|
68
|
+
full_name = self.class.name.to_s
|
|
69
|
+
ns = full_name.split("::")
|
|
70
|
+
ns.each do |n|
|
|
71
|
+
nss << n
|
|
72
|
+
end
|
|
73
|
+
nss.pop
|
|
74
|
+
nss
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
end
|
data/lib/m_string.rb
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
class String
|
|
2
|
+
|
|
3
|
+
def starts_with?(x)
|
|
4
|
+
self.match(/^#{x}/) ? true : false
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def ends_with?(x)
|
|
8
|
+
self.match(/#{x}$/) ? true : false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Returns "is" or "are" based on the number, i.e. "i=6; There #{isare(i)} #{i} topics here."
|
|
12
|
+
def self.pluralize_word(num, vals = ["is", "are"])
|
|
13
|
+
return vals[0] if num.to_i==1
|
|
14
|
+
return vals[1]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def remove(val)
|
|
18
|
+
gsub(val, '')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def remove!(val)
|
|
22
|
+
gsub!(val, '')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# makes long strings of unbroken characters wrap inside elements (hopefully! tested in Safari, Firefox, and IE for Windows)
|
|
26
|
+
# For documentation about this kind of workaround to this browser compatibility problem please see the following URL's:
|
|
27
|
+
# http://www.quirksmode.org/oddsandends/wbr.html
|
|
28
|
+
# http://krijnhoetmer.nl/stuff/css/word-wrap-break-word/
|
|
29
|
+
# note: if the txt has spaces, this will only try to break up runs of non-space characters longer than "every" characters
|
|
30
|
+
def breakify(every = 30)
|
|
31
|
+
every = 1 if every < 1
|
|
32
|
+
text = self
|
|
33
|
+
brokentxt = text.gsub(/\S{#{every},}/) { |longword| longword.scan(/.{1,#{every}}/).join("<wbr/>") }
|
|
34
|
+
#text = %Q[<span style='word-wrap:break-word;wbr:after{content:"\\00200B"}'>#{brokentxt}</span>]
|
|
35
|
+
#brokentxt.gsub("<wbr/>", "<br />")
|
|
36
|
+
brokentxt.gsub("<wbr/>", " ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def breakify!(every = 30)
|
|
40
|
+
self.replace(self.breakify(every))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
self.underscore.split('_').join(' ').humanize
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def hexdigest
|
|
49
|
+
Digest::SHA1.hexdigest(self.downcase)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def hexdigest!
|
|
53
|
+
self.replace(self.hexdigest)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def equal_ignore_case(x)
|
|
57
|
+
self.downcase == x.downcase if !x.nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def h_url
|
|
61
|
+
x = ""
|
|
62
|
+
word_size = 5
|
|
63
|
+
while x.blank?
|
|
64
|
+
x = self.gsub(/([^ a-zA-Z0-9_-]+)/n, '').tr(' ', '-').gsub(/--/, '-').downcase#.truncate(100, "")
|
|
65
|
+
x = x.split('-').collect {|word| word if word.size >= word_size}.compact[0..3].join("-")
|
|
66
|
+
x = x.truncate(100, "")
|
|
67
|
+
word_size -= 1
|
|
68
|
+
return self if word_size == -1
|
|
69
|
+
end
|
|
70
|
+
x
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def h_url!
|
|
74
|
+
self.replace(self.h_url)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.randomize(length = 10)
|
|
78
|
+
chars = ("A".."H").to_a + ("J".."N").to_a + ("P".."T").to_a + ("W".."Z").to_a + ("3".."9").to_a
|
|
79
|
+
newpass = ""
|
|
80
|
+
1.upto(length) { |i| newpass << chars[rand(chars.size-1)] }
|
|
81
|
+
return newpass.upcase
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def truncate(length = 30, truncate_string = "...")
|
|
85
|
+
if self.nil? then return end
|
|
86
|
+
l = length - truncate_string.length
|
|
87
|
+
if $KCODE == "NONE"
|
|
88
|
+
self.length > length ? self[0...l] + truncate_string : self
|
|
89
|
+
else
|
|
90
|
+
chars = self.split(//)
|
|
91
|
+
chars.length > length ? chars[0...l].join + truncate_string : self
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def truncate!(length = 30, truncate_string = "...")
|
|
96
|
+
self.replace(self.truncate(length, truncate_string))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def capitalize_all_words
|
|
100
|
+
self.gsub(/\b\w/) {|s| s.upcase}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def capitalize_all_words!
|
|
104
|
+
self.replace(self.capitalize_all_words)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# remove_bad_chars!: any characters not in the allowed set will be removed
|
|
108
|
+
# squeeze: remove any repeated dash, underscore, period, comma characters
|
|
109
|
+
def remove_bad_chars
|
|
110
|
+
#ic = Iconv.new('UTF-8//IGNORE', 'UTF-8') # ignore bytes you can't convert
|
|
111
|
+
#str = ic.iconv(self + ' ')[0..-2]
|
|
112
|
+
if !self.nil?
|
|
113
|
+
x = self.gsub(/[^\s0-9\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\_\~\`\^\|\\ \{\}\[\]\’\“\” A-Za-z]/, '').to_s.gsub(/[\’]/, "'").to_s.gsub(/[\“]/, '"').to_s.gsub(/[\”]/, '"').squeeze(",").squeeze("-").squeeze("!").squeeze("*").squeeze("?")
|
|
114
|
+
|
|
115
|
+
# now use the WhiteListHelper plugin to clean out html encodings
|
|
116
|
+
white_list(x)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def remove_bad_chars!
|
|
122
|
+
self.replace(self.remove_bad_chars)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# keep adding on to this whenever it becomes apparent that unsafe strings
|
|
126
|
+
# could get passed through into the database
|
|
127
|
+
def sql_safe_str
|
|
128
|
+
if !self.nil?
|
|
129
|
+
self.gsub(/[\']/, "\'\'")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def sql_safe_str!
|
|
134
|
+
self.replace(self.sql_safe_str)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'find'
|
|
2
|
+
path = File.join(RAILS_ROOT, "vendor", "plugins", "ruby_extensions", "lib")
|
|
3
|
+
Find.find(path) do |f|
|
|
4
|
+
if FileTest.directory?(f) and f =~ /\.svn/
|
|
5
|
+
Find.prune
|
|
6
|
+
else
|
|
7
|
+
if FileTest.file?(f)
|
|
8
|
+
f.gsub!(path, "")
|
|
9
|
+
m = f.match(/\/[a-zA\-Z-_]*.rb/)
|
|
10
|
+
if m
|
|
11
|
+
model = m.to_s
|
|
12
|
+
x = model.gsub('/', '').gsub('.rb', '')
|
|
13
|
+
begin
|
|
14
|
+
require x
|
|
15
|
+
rescue => ex
|
|
16
|
+
puts ex
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
rubygems_version: 0.9.2
|
|
3
|
+
specification_version: 1
|
|
4
|
+
name: ruby_extensions
|
|
5
|
+
version: !ruby/object:Gem::Version
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
date: 2007-09-20 00:00:00 -04:00
|
|
8
|
+
summary: ruby_extensions
|
|
9
|
+
require_paths:
|
|
10
|
+
- lib
|
|
11
|
+
- lib
|
|
12
|
+
- lib
|
|
13
|
+
- lib/tasks
|
|
14
|
+
email:
|
|
15
|
+
homepage:
|
|
16
|
+
rubyforge_project: magrathea
|
|
17
|
+
description: "ruby_extensions was developed by: markbates"
|
|
18
|
+
autorequire:
|
|
19
|
+
- ruby_extensions
|
|
20
|
+
- m_string
|
|
21
|
+
- m_object
|
|
22
|
+
- m_math
|
|
23
|
+
- m_logger
|
|
24
|
+
- m_kernel
|
|
25
|
+
- m_float
|
|
26
|
+
- m_dir
|
|
27
|
+
- m_class
|
|
28
|
+
- m_array
|
|
29
|
+
- m_string
|
|
30
|
+
- m_object
|
|
31
|
+
- m_math
|
|
32
|
+
- m_logger
|
|
33
|
+
- m_kernel
|
|
34
|
+
- m_float
|
|
35
|
+
- m_dir
|
|
36
|
+
- m_class
|
|
37
|
+
- m_array
|
|
38
|
+
default_executable:
|
|
39
|
+
bindir: bin
|
|
40
|
+
has_rdoc: false
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 0.0.0
|
|
46
|
+
version:
|
|
47
|
+
platform: ruby
|
|
48
|
+
signing_key:
|
|
49
|
+
cert_chain:
|
|
50
|
+
post_install_message:
|
|
51
|
+
authors:
|
|
52
|
+
- markbates
|
|
53
|
+
files:
|
|
54
|
+
- init.rb
|
|
55
|
+
- lib/m_array.rb
|
|
56
|
+
- lib/m_class.rb
|
|
57
|
+
- lib/m_dir.rb
|
|
58
|
+
- lib/m_float.rb
|
|
59
|
+
- lib/m_kernel.rb
|
|
60
|
+
- lib/m_logger.rb
|
|
61
|
+
- lib/m_math.rb
|
|
62
|
+
- lib/m_object.rb
|
|
63
|
+
- lib/m_string.rb
|
|
64
|
+
- lib/ruby_extensions.rb
|
|
65
|
+
- lib/tasks/rubyforge_config.yml
|
|
66
|
+
test_files: []
|
|
67
|
+
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
|
|
70
|
+
extra_rdoc_files: []
|
|
71
|
+
|
|
72
|
+
executables: []
|
|
73
|
+
|
|
74
|
+
extensions: []
|
|
75
|
+
|
|
76
|
+
requirements: []
|
|
77
|
+
|
|
78
|
+
dependencies: []
|
|
79
|
+
|