na 1.2.37 → 1.2.38
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.md +22 -0
- data/Gemfile.lock +1 -1
- data/README.md +79 -6
- data/bin/commands/add.rb +11 -13
- data/bin/commands/edit.rb +15 -19
- data/bin/commands/find.rb +11 -6
- data/bin/commands/init.rb +1 -1
- data/bin/commands/next.rb +56 -27
- data/bin/commands/projects.rb +1 -1
- data/bin/commands/saved.rb +3 -4
- data/bin/commands/tagged.rb +7 -7
- data/bin/commands/todos.rb +24 -14
- data/bin/commands/update.rb +24 -27
- data/bin/na +2 -1
- data/lib/na/action.rb +16 -25
- data/lib/na/actions.rb +8 -8
- data/lib/na/colors.rb +23 -1
- data/lib/na/editor.rb +13 -11
- data/lib/na/hash.rb +31 -0
- data/lib/na/next_action.rb +45 -38
- data/lib/na/pager.rb +1 -1
- data/lib/na/prompt.rb +6 -6
- data/lib/na/string.rb +23 -3
- data/lib/na/theme.rb +71 -0
- data/lib/na/todo.rb +2 -2
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +1 -0
- data/src/_README.md +35 -15
- metadata +3 -2
data/bin/commands/todos.rb
CHANGED
@@ -8,24 +8,34 @@ class App
|
|
8
8
|
/, :, or a space, e.g. `na todos code/marked`'
|
9
9
|
arg_name 'QUERY', optional: true
|
10
10
|
command %i[todos] do |c|
|
11
|
-
c.
|
12
|
-
|
13
|
-
all_req = args.join(' ') !~ /[+!-]/
|
11
|
+
c.desc 'Open the todo database in an editor for manual modification'
|
12
|
+
c.switch %i[e edit]
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
14
|
+
c.action do |_global_options, options, args|
|
15
|
+
if options[:edit]
|
16
|
+
system("#{NA::Editor.default_editor(prefer_git_editor: false)} #{NA.database_path}")
|
17
|
+
editor = NA::Editor.default_editor(prefer_git_editor: false).highlight_filename
|
18
|
+
database = NA.database_path.highlight_filename
|
19
|
+
NA.notify("{b}#{NA.theme[:success]}Opened #{database}#{NA.theme[:success]} in #{editor}")
|
20
|
+
else
|
21
|
+
if args.count.positive?
|
22
|
+
all_req = args.join(' ') !~ /(?<=[, ])[+!-]/
|
23
|
+
|
24
|
+
tokens = [{ token: '*', required: all_req, negate: false }]
|
25
|
+
args.each do |arg|
|
26
|
+
arg.split(/ *, */).each do |a|
|
27
|
+
m = a.match(/^(?<req>[+!-])?(?<tok>.*?)$/)
|
28
|
+
tokens.push({
|
29
|
+
token: m['tok'],
|
30
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
31
|
+
negate: (!m['req'].nil? && m['req'] =~ /[!-]/) ? true : false
|
32
|
+
})
|
33
|
+
end
|
24
34
|
end
|
25
35
|
end
|
26
|
-
end
|
27
36
|
|
28
|
-
|
37
|
+
NA.list_todos(query: tokens)
|
38
|
+
end
|
29
39
|
end
|
30
40
|
end
|
31
41
|
end
|
data/bin/commands/update.rb
CHANGED
@@ -111,8 +111,8 @@ class App
|
|
111
111
|
]
|
112
112
|
`gum input #{opts.join(' ')}`.strip
|
113
113
|
elsif $stdin.isatty && options[:tagged].empty?
|
114
|
-
|
115
|
-
reader.read_line(NA::Color.template(
|
114
|
+
NA.notify("#{NA.theme[:prompt]}Enter search string:")
|
115
|
+
reader.read_line(NA::Color.template("#{NA.theme[:filename]}> #{NA.theme[:action]}")).strip
|
116
116
|
end
|
117
117
|
|
118
118
|
if action
|
@@ -123,35 +123,34 @@ class App
|
|
123
123
|
tokens = Regexp.new(action, Regexp::IGNORECASE)
|
124
124
|
else
|
125
125
|
tokens = []
|
126
|
-
all_req = action !~ /[
|
126
|
+
all_req = action !~ /[+!-]/ && !options[:or]
|
127
127
|
|
128
128
|
action.split(/ /).each do |arg|
|
129
129
|
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
130
130
|
tokens.push({
|
131
131
|
token: m['tok'],
|
132
132
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
133
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
133
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
134
134
|
})
|
135
135
|
end
|
136
136
|
end
|
137
137
|
end
|
138
138
|
|
139
139
|
if (action.nil? || action.empty?) && options[:tagged].empty?
|
140
|
-
|
141
|
-
Process.exit 1
|
140
|
+
NA.notify("#{NA.theme[:error]}Empty input, cancelled", exit_code: 1)
|
142
141
|
end
|
143
142
|
|
144
|
-
all_req = options[:tagged].join(' ') !~ /[
|
143
|
+
all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
|
145
144
|
tags = []
|
146
145
|
options[:tagged].join(',').split(/ *, */).each do |arg|
|
147
|
-
m = arg.match(/^(?<req>[
|
146
|
+
m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$*~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
|
148
147
|
|
149
148
|
tags.push({
|
150
149
|
tag: m['tag'].wildcard_to_rx,
|
151
150
|
comp: m['op'],
|
152
151
|
value: m['val'],
|
153
152
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
154
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
153
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
155
154
|
})
|
156
155
|
end
|
157
156
|
|
@@ -170,7 +169,7 @@ class App
|
|
170
169
|
args << '--width $(tput cols)'
|
171
170
|
`gum write #{args.join(' ')}`.strip.split("\n")
|
172
171
|
else
|
173
|
-
|
172
|
+
NA.notify("#{NA.theme[:prompt]}Enter a note, {bw}CTRL-d#{NA.theme[:prompt]} to end editing:#{NA.theme[:action]}")
|
174
173
|
reader.read_multiline
|
175
174
|
end
|
176
175
|
end
|
@@ -182,13 +181,11 @@ class App
|
|
182
181
|
options[:project]
|
183
182
|
elsif NA.cwd_is == :project
|
184
183
|
NA.cwd
|
185
|
-
else
|
186
|
-
nil
|
187
184
|
end
|
188
185
|
|
189
186
|
if options[:file]
|
190
187
|
file = File.expand_path(options[:file])
|
191
|
-
NA.notify(
|
188
|
+
NA.notify("#{NA.theme[:error]}File not found", exit_code: 1) unless File.exist?(file)
|
192
189
|
|
193
190
|
targets = [file]
|
194
191
|
elsif options[:todo]
|
@@ -198,7 +195,7 @@ class App
|
|
198
195
|
todo.push({
|
199
196
|
token: m['tok'],
|
200
197
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
201
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
198
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
202
199
|
})
|
203
200
|
end
|
204
201
|
dirs = NA.match_working_dir(todo)
|
@@ -207,25 +204,25 @@ class App
|
|
207
204
|
targets = [dirs[0]]
|
208
205
|
elsif dirs.count.positive?
|
209
206
|
targets = NA.select_file(dirs, multiple: true)
|
210
|
-
NA.notify(
|
207
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless targets && targets.count.positive?
|
211
208
|
else
|
212
|
-
NA.notify(
|
209
|
+
NA.notify("#{NA.theme[:error]}Todo not found", exit_code: 1) unless targets && targets.count.positive?
|
213
210
|
|
214
211
|
end
|
215
212
|
else
|
216
213
|
files = NA.find_files_matching({
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
NA.notify(
|
214
|
+
depth: options[:depth],
|
215
|
+
done: options[:done],
|
216
|
+
project: target_proj,
|
217
|
+
regex: options[:regex],
|
218
|
+
require_na: false,
|
219
|
+
search: tokens,
|
220
|
+
tag: tags
|
221
|
+
})
|
222
|
+
NA.notify("#{NA.theme[:error]}No todo file found", exit_code: 1) if files.count.zero?
|
226
223
|
|
227
224
|
targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
|
228
|
-
NA.notify(
|
225
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless files.count.positive?
|
229
226
|
|
230
227
|
end
|
231
228
|
|
@@ -234,7 +231,7 @@ class App
|
|
234
231
|
options[:project] = 'Archive'
|
235
232
|
end
|
236
233
|
|
237
|
-
NA.notify(
|
234
|
+
NA.notify("#{NA.theme[:error]}No search terms provided", exit_code: 1) if tokens.nil? && options[:tagged].empty?
|
238
235
|
|
239
236
|
targets.each do |target|
|
240
237
|
NA.update_action(target, tokens,
|
data/bin/na
CHANGED
@@ -37,6 +37,7 @@ class App
|
|
37
37
|
default_command :next
|
38
38
|
|
39
39
|
NA::Color.coloring = $stdin.isatty
|
40
|
+
NA::Pager.paginate = $stdin.isatty
|
40
41
|
|
41
42
|
desc 'Add a next action (deprecated, for backwards compatibility)'
|
42
43
|
switch %i[a add], negatable: false
|
@@ -84,7 +85,7 @@ class App
|
|
84
85
|
|
85
86
|
pre do |global, _command, _options, _args|
|
86
87
|
NA.verbose = global[:debug]
|
87
|
-
NA::Pager.paginate = global[:pager]
|
88
|
+
NA::Pager.paginate = global[:pager] && $stdout.isatty
|
88
89
|
NA::Color.coloring = global[:color]
|
89
90
|
NA.extension = global[:ext]
|
90
91
|
NA.na_tag = global[:na_tag]
|
data/lib/na/action.rb
CHANGED
@@ -26,15 +26,15 @@ module NA
|
|
26
26
|
string += " @priority(#{priority})"
|
27
27
|
end
|
28
28
|
|
29
|
-
|
29
|
+
remove_tag.each do |tag|
|
30
30
|
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
31
31
|
string.strip!
|
32
|
-
string += " @#{tag}"
|
33
32
|
end
|
34
33
|
|
35
|
-
|
34
|
+
add_tag.each do |tag|
|
36
35
|
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
37
36
|
string.strip!
|
37
|
+
string += " @#{tag}"
|
38
38
|
end
|
39
39
|
|
40
40
|
string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})" if finish && string !~ /(?<=\A| )@done/
|
@@ -74,25 +74,14 @@ module NA
|
|
74
74
|
## @param notes [Boolean] Include notes
|
75
75
|
##
|
76
76
|
def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false)
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
parent: '{c}',
|
81
|
-
parent_divider: '{xw}/',
|
82
|
-
action: '{bg}',
|
83
|
-
project: '{xbk}',
|
84
|
-
tags: '{m}',
|
85
|
-
value_parens: '{m}',
|
86
|
-
values: '{y}',
|
87
|
-
output: '%filename%parents| %action',
|
88
|
-
note: '{dw}'
|
89
|
-
}
|
90
|
-
template = default_template.merge(template)
|
77
|
+
theme = NA::Theme.load_theme
|
78
|
+
template = theme.merge(template)
|
79
|
+
|
91
80
|
# Create the hierarchical parent string
|
92
81
|
parents = @parent.map do |par|
|
93
|
-
NA::Color.template("#{template[:parent]}#{par}")
|
82
|
+
NA::Color.template("{x}#{template[:parent]}#{par}")
|
94
83
|
end.join(NA::Color.template(template[:parent_divider]))
|
95
|
-
parents = "{
|
84
|
+
parents = "#{NA.theme[:bracket]}[#{NA.theme[:error]}#{parents}#{NA.theme[:bracket]}]{x} "
|
96
85
|
|
97
86
|
# Create the project string
|
98
87
|
project = NA::Color.template("#{template[:project]}#{@project}{x} ")
|
@@ -101,7 +90,7 @@ module NA
|
|
101
90
|
file = @file.sub(%r{^\./}, '').sub(/#{ENV['HOME']}/, '~')
|
102
91
|
file = file.sub(/\.#{extension}$/, '')
|
103
92
|
# colorize the basename
|
104
|
-
file = file.
|
93
|
+
file = file.highlight_filename
|
105
94
|
file_tpl = "#{template[:file]}#{file} {x}"
|
106
95
|
filename = NA::Color.template(file_tpl)
|
107
96
|
|
@@ -200,13 +189,13 @@ module NA
|
|
200
189
|
tag_date = Time.parse(tag_val)
|
201
190
|
date = Chronic.parse(val)
|
202
191
|
|
192
|
+
raise ArgumentError if date.nil?
|
193
|
+
|
203
194
|
unless val =~ /(\d:\d|a[mp]|now)/i
|
204
195
|
tag_date = Time.parse(tag_date.strftime('%Y-%m-%d 12:00'))
|
205
196
|
date = Time.parse(date.strftime('%Y-%m-%d 12:00'))
|
206
197
|
end
|
207
198
|
|
208
|
-
# NA.notify("{dw}Comparing #{tag_date} #{tag[:comp]} #{date}{x}", debug: true)
|
209
|
-
|
210
199
|
case tag[:comp]
|
211
200
|
when /^>$/
|
212
201
|
tag_date > date
|
@@ -227,7 +216,7 @@ module NA
|
|
227
216
|
else
|
228
217
|
false
|
229
218
|
end
|
230
|
-
rescue
|
219
|
+
rescue ArgumentError
|
231
220
|
case tag[:comp]
|
232
221
|
when /^>$/
|
233
222
|
tag_val.to_f > val.to_f
|
@@ -239,12 +228,14 @@ module NA
|
|
239
228
|
tag_val.to_f >= val.to_f
|
240
229
|
when /^==?$/
|
241
230
|
tag_val =~ /^#{val.wildcard_to_rx}$/
|
231
|
+
when /^=~$/
|
232
|
+
tag_val =~ Regexp.new(val, Regexp::IGNORECASE)
|
242
233
|
when /^\$=$/
|
243
234
|
tag_val =~ /#{val.wildcard_to_rx}$/i
|
244
235
|
when /^\*=$/
|
245
|
-
tag_val =~
|
236
|
+
tag_val =~ /.*?#{val.wildcard_to_rx}.*?/i
|
246
237
|
when /^\^=$/
|
247
|
-
tag_val =~ /^#{val.wildcard_to_rx}/
|
238
|
+
tag_val =~ /^#{val.wildcard_to_rx}/i
|
248
239
|
else
|
249
240
|
false
|
250
241
|
end
|
data/lib/na/actions.rb
CHANGED
@@ -20,7 +20,7 @@ module NA
|
|
20
20
|
return if files.nil?
|
21
21
|
|
22
22
|
if nest
|
23
|
-
template =
|
23
|
+
template = NA.theme[:templates][:default]
|
24
24
|
|
25
25
|
parent_files = {}
|
26
26
|
out = []
|
@@ -40,7 +40,7 @@ module NA
|
|
40
40
|
out.concat(NA.output_children(projects, 0))
|
41
41
|
end
|
42
42
|
else
|
43
|
-
template =
|
43
|
+
template = NA.theme[:templates][:default]
|
44
44
|
|
45
45
|
each do |action|
|
46
46
|
if parent_files.key?(action.file)
|
@@ -62,22 +62,22 @@ module NA
|
|
62
62
|
else
|
63
63
|
template = if files.count.positive?
|
64
64
|
if files.count == 1
|
65
|
-
|
65
|
+
NA.theme[:templates][:single_file]
|
66
66
|
else
|
67
|
-
|
67
|
+
NA.theme[:templates][:multi_file]
|
68
68
|
end
|
69
69
|
elsif NA.find_files(depth: depth).count > 1
|
70
70
|
if depth > 1
|
71
|
-
|
71
|
+
NA.theme[:templates][:multi_file]
|
72
72
|
else
|
73
|
-
|
73
|
+
NA.theme[:templates][:single_file]
|
74
74
|
end
|
75
75
|
else
|
76
|
-
|
76
|
+
NA.theme[:templates][:default]
|
77
77
|
end
|
78
78
|
template += '%note' if notes
|
79
79
|
|
80
|
-
files.map { |f| NA.notify(
|
80
|
+
files.map { |f| NA.notify(f, debug: true) } if files
|
81
81
|
|
82
82
|
output = map { |action| action.pretty(template: { output: template }, regexes: regexes, notes: notes) }
|
83
83
|
NA::Pager.page(output.join("\n"))
|
data/lib/na/colors.rb
CHANGED
@@ -5,7 +5,7 @@ module NA
|
|
5
5
|
# Terminal output color functions.
|
6
6
|
module Color
|
7
7
|
# Regexp to match excape sequences
|
8
|
-
ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)
|
8
|
+
ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)/
|
9
9
|
|
10
10
|
# All available color names. Available as methods and string extensions.
|
11
11
|
#
|
@@ -214,9 +214,15 @@ module NA
|
|
214
214
|
## m: magenta, r: red, b: bold, u: underline, i: italic,
|
215
215
|
## x: reset (remove background, color, emphasis)
|
216
216
|
##
|
217
|
+
## Also accepts {#RGB} and {#RRGGBB} strings. Put a b before
|
218
|
+
## the hash to make it a background color
|
219
|
+
##
|
217
220
|
## @example Convert a templated string
|
218
221
|
## Color.template('{Rwb}Warning:{x} {w}you look a little {g}ill{x}')
|
219
222
|
##
|
223
|
+
## @example Convert using RGB colors
|
224
|
+
## Color.template('{#f0a}This is an RGB color')
|
225
|
+
##
|
220
226
|
## @param input [String, Array] The template
|
221
227
|
## string. If this is an array, the
|
222
228
|
## elements will be joined with a
|
@@ -228,6 +234,11 @@ module NA
|
|
228
234
|
input = input.join(' ') if input.is_a? Array
|
229
235
|
return input.gsub(/(?<!\\)\{(\w+)\}/i, '') unless NA::Color.coloring?
|
230
236
|
|
237
|
+
input = input.gsub(/(?<!\\)\{((?:[fb]g?)?#[a-f0-9]{3,6})\}/i) do
|
238
|
+
hex = Regexp.last_match(1)
|
239
|
+
rgb(hex)
|
240
|
+
end
|
241
|
+
|
231
242
|
fmt = input.gsub(/%/, '%%')
|
232
243
|
fmt = fmt.gsub(/(?<!\\)\{(\w+)\}/i) do
|
233
244
|
Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
|
@@ -301,6 +312,17 @@ module NA
|
|
301
312
|
is_bg = hex.match(/^bg?#/) ? true : false
|
302
313
|
hex_string = hex.sub(/^([fb]g?)?#/, '')
|
303
314
|
|
315
|
+
if hex_string.length == 3
|
316
|
+
parts = hex_string.match(/(?<r>.)(?<g>.)(?<b>.)/)
|
317
|
+
|
318
|
+
t = []
|
319
|
+
%w[r g b].each do |e|
|
320
|
+
t << parts[e]
|
321
|
+
t << parts[e]
|
322
|
+
end
|
323
|
+
hex_string = t.join('')
|
324
|
+
end
|
325
|
+
|
304
326
|
parts = hex_string.match(/(?<r>..)(?<g>..)(?<b>..)/)
|
305
327
|
t = []
|
306
328
|
%w[r g b].each do |e|
|
data/lib/na/editor.rb
CHANGED
@@ -1,24 +1,26 @@
|
|
1
1
|
module NA
|
2
2
|
module Editor
|
3
3
|
class << self
|
4
|
-
def default_editor
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
def default_editor(prefer_git_editor: true)
|
5
|
+
if prefer_git_editor
|
6
|
+
editor ||= ENV['NA_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
|
7
|
+
else
|
8
|
+
editor ||= ENV['NA_EDITOR'] || ENV['EDITOR'] || ENV['GIT_EDITOR']
|
9
9
|
end
|
10
10
|
|
11
|
-
|
11
|
+
return editor if editor&.good? && TTY::Which.exist?(editor)
|
12
|
+
|
13
|
+
NA.notify("No EDITOR environment variable, testing available editors", debug: true)
|
12
14
|
editors = %w[vim vi code subl mate mvim nano emacs]
|
13
15
|
editors.each do |ed|
|
14
16
|
try = TTY::Which.which(ed)
|
15
17
|
if try
|
16
|
-
notify("Using editor #{try}", debug: true)
|
18
|
+
NA.notify("Using editor #{try}", debug: true)
|
17
19
|
return try
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
|
-
notify(
|
23
|
+
NA.notify("#{NA.theme[:error]}No editor found", exit_code: 5)
|
22
24
|
|
23
25
|
nil
|
24
26
|
end
|
@@ -49,7 +51,7 @@ module NA
|
|
49
51
|
def fork_editor(input = '', message: :default)
|
50
52
|
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
51
53
|
|
52
|
-
notify(
|
54
|
+
NA.notify("#{NA.theme[:error]}No EDITOR variable defined in environment", exit_code: 5) if default_editor.nil?
|
53
55
|
|
54
56
|
tmpfile = Tempfile.new(['na_temp', '.na'])
|
55
57
|
|
@@ -97,11 +99,11 @@ module NA
|
|
97
99
|
## @return [Array] [[String]title, [Note]note]
|
98
100
|
##
|
99
101
|
def format_input(input)
|
100
|
-
notify(
|
102
|
+
NA.notify("#{NA.theme[:error]}No content in entry", exit_code: 1) if input.nil? || input.strip.empty?
|
101
103
|
|
102
104
|
input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
|
103
105
|
title = input_lines[0]&.strip
|
104
|
-
notify(
|
106
|
+
NA.notify("#{NA.theme[:error]}No content in first line", exit_code: 1) if title.nil? || title.strip.empty?
|
105
107
|
|
106
108
|
title.expand_date_tags
|
107
109
|
|
data/lib/na/hash.rb
CHANGED
@@ -4,4 +4,35 @@ class ::Hash
|
|
4
4
|
def symbolize_keys
|
5
5
|
each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
|
6
6
|
end
|
7
|
+
|
8
|
+
##
|
9
|
+
## Freeze all values in a hash
|
10
|
+
##
|
11
|
+
## @return Hash with all values frozen
|
12
|
+
##
|
13
|
+
def deep_freeze
|
14
|
+
chilled = {}
|
15
|
+
each do |k, v|
|
16
|
+
chilled[k] = v.is_a?(Hash) ? v.deep_freeze : v.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
chilled.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
def deep_freeze!
|
23
|
+
replace deep_thaw.deep_freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
def deep_thaw
|
27
|
+
chilled = {}
|
28
|
+
each do |k, v|
|
29
|
+
chilled[k] = v.is_a?(Hash) ? v.deep_thaw : v.dup
|
30
|
+
end
|
31
|
+
|
32
|
+
chilled.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def deep_thaw!
|
36
|
+
replace deep_thaw
|
37
|
+
end
|
7
38
|
end
|