fled 0.0.2 → 0.0.3
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/README.md +459 -89
- data/bin/fled +341 -53
- data/lib/dtc/utils/exec.rb +13 -2
- data/lib/dtc/utils/interactive_edit.rb +10 -0
- data/lib/dtc/utils/meta.rb +47 -0
- data/lib/dtc/utils/text/html.rb +107 -0
- data/lib/dtc/utils/text/line_writer.rb +70 -0
- data/lib/dtc/utils/text.rb +23 -0
- data/lib/dtc/utils/visitor/dsl.rb +98 -0
- data/lib/dtc/utils/visitor/folder.rb +87 -0
- data/lib/dtc/utils/visitor.rb +272 -0
- data/lib/dtc/utils.rb +3 -1
- data/lib/fled/file_listing.rb +35 -62
- data/lib/fled/file_listing_builder.rb +43 -0
- data/lib/fled.rb +18 -5
- data/tests/helper.rb +14 -26
- data/tests/readme.rb +139 -85
- metadata +9 -3
- data/lib/dtc/utils/dsldsl.rb +0 -259
- data/lib/dtc/utils/file_visitor.rb +0 -79
data/bin/fled
CHANGED
@@ -14,13 +14,62 @@ class App
|
|
14
14
|
DEFAULT_SKIP_FOLDERS = ['.svn', '_svn', '.git', 'CVS', '.hg']
|
15
15
|
DEFAULT_SKIP_FILES_RX = ['^\..*', '.*~$']
|
16
16
|
DEFAULT_SKIP_FILES = [".DS_Store", "Thumbs.db", "Temporary Items"]
|
17
|
-
|
17
|
+
DEFAULT_OPTIONS_PATH = "~/.fled.yaml"
|
18
|
+
DEFAULT_OPTIONS = {
|
19
|
+
:max_depth => -1,
|
20
|
+
:interactive => true,
|
21
|
+
:verbose => false,
|
22
|
+
:diff_command => %w[diff -uda],
|
23
|
+
:excluded_files =>
|
24
|
+
DEFAULT_SKIP_FILES_RX +
|
25
|
+
DEFAULT_SKIP_FILES.map { |e| '\A' + Regexp.escape(e) + '\z' },
|
26
|
+
:excluded_directories =>
|
27
|
+
DEFAULT_SKIP_FOLDERS_RX +
|
28
|
+
DEFAULT_SKIP_FOLDERS.map { |e| '\A' + Regexp.escape(e) + '\z' },
|
29
|
+
}
|
30
|
+
OPTIONS_THAT_AFFECT_SCANNING = [
|
31
|
+
:max_depth,
|
32
|
+
:excluded, :excluded_directories, :excluded_files,
|
33
|
+
:included, :included_directories, :included_files,
|
34
|
+
]
|
35
|
+
OPTIONS_THAT_CANT_CHANGE_DURING_INTERACTIVE_RUN = [ :interactive ]
|
36
|
+
EXIT_REASONS = {
|
37
|
+
:nothing_to_list => ["No files/folders found", 1],
|
38
|
+
:nothing_edited => ["No changes", 2],
|
39
|
+
:user_abort => ["User abort", 126],
|
40
|
+
:invalid_options => ["Error parsing arguments", 127, :print_usage],
|
41
|
+
}
|
42
|
+
MAIN_MENU_OPTIONS = {
|
43
|
+
:reconfigure => "Re-sca(n)",
|
44
|
+
:edit_listing => "(E)dit listing",
|
45
|
+
:abort => "(A)bort",
|
46
|
+
}
|
47
|
+
MAIN_MENU_OPTIONS_WITH_OPERATIONS = {
|
48
|
+
:diff => "(D)iff",
|
49
|
+
:edit_script => "Edit (s)cript",
|
50
|
+
:print_preview => "(P)review script",
|
51
|
+
:quit => "Print and (q)uit",
|
52
|
+
:revert_listing => "(R)evert",
|
53
|
+
}
|
54
|
+
MAIN_MENU_OPTIONS_AFTER_SCRIPT_EDIT = {
|
55
|
+
:quit => "Print edited script and (q)uit",
|
56
|
+
:edit_script => "(E)dit script",
|
57
|
+
:revert_script => "(R)evert script and edit",
|
58
|
+
:return => "(D)iscard edited script",
|
59
|
+
:abort => "(A)bort",
|
60
|
+
}
|
61
|
+
CONFIRM_MENU_OPTIONS_FOR_RESCAN = {
|
62
|
+
:yes => "(Y)es, loose any edits and uids and rescan",
|
63
|
+
:no => "(N)o, cancel",
|
64
|
+
}
|
18
65
|
attr_reader :options
|
19
66
|
|
20
67
|
def initialize(arguments = ARGV)
|
21
68
|
@arguments = arguments.dup
|
22
69
|
@options = {}
|
23
70
|
@options[:verbose] = false
|
71
|
+
@edited_listing = nil
|
72
|
+
@operations = []
|
24
73
|
end
|
25
74
|
|
26
75
|
def run
|
@@ -30,49 +79,211 @@ class App
|
|
30
79
|
exit 0
|
31
80
|
end
|
32
81
|
output_version if @options[:verbose]
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
42
|
-
result = DTC::Utils::InteractiveEditor::edit(content, ".yaml")
|
43
|
-
if result.nil? || result.strip == "" || result == content
|
44
|
-
$stderr.puts "No changes - aborting" ; exit 2
|
45
|
-
end
|
46
|
-
target_listing = FlEd::FileListing.parse(result)
|
47
|
-
ops = target_listing.operations_from!(builder.listing)
|
48
|
-
if ops.empty?
|
49
|
-
$stderr.puts "No changes - aborting" ; exit 2
|
50
|
-
end
|
51
|
-
ops = [[:pushd, @base_path]] + ops + [[:popd]] unless @options[:no_pushd]
|
52
|
-
puts FlEd::operation_list_to_bash(ops).join("\n")
|
53
|
-
$stderr.puts "\nFinished at #{DateTime.now}" if @options[:verbose]
|
82
|
+
update_listing!
|
83
|
+
edit_listing!
|
84
|
+
if @options[:interactive]
|
85
|
+
interactive_main_menu
|
86
|
+
else
|
87
|
+
die! :nothing_edited if operations.empty?
|
88
|
+
print_script
|
89
|
+
end
|
54
90
|
else
|
55
91
|
output_usage
|
56
92
|
end
|
57
93
|
rescue SystemExit => e
|
58
94
|
raise
|
59
|
-
rescue
|
60
|
-
|
61
|
-
output_usage
|
62
|
-
exit 127
|
95
|
+
rescue OptionParser::ParseError => e
|
96
|
+
die! :invalid_options, e.to_s
|
63
97
|
end
|
98
|
+
|
64
99
|
protected
|
100
|
+
def update_listing!
|
101
|
+
@listing = scan_path_into_listing(@base_path)
|
102
|
+
@listing_text = @listing.to_s
|
103
|
+
@edited_listing = nil
|
104
|
+
@operations = []
|
105
|
+
end
|
106
|
+
|
107
|
+
def update_operations! edited_listing = @edited_listing
|
108
|
+
@operations = ops_for_edited_listing(@listing, @listing_text, edited_listing)
|
109
|
+
end
|
110
|
+
|
111
|
+
def edit_listing!
|
112
|
+
edited_listing = DTC::Utils::InteractiveEditor::edit(@edited_listing || @listing_text, ".yaml", @options[:editor])
|
113
|
+
@edited_listing = edited_listing unless edited_listing.to_s.strip == ""
|
114
|
+
update_operations! edited_listing
|
115
|
+
interactive_print_status
|
116
|
+
end
|
117
|
+
|
118
|
+
def print_script operations = @operations, preview = false
|
119
|
+
indentation = preview ? " " : ""
|
120
|
+
script = operations.is_a?(String) ? operations : script_for_operations(operations, indentation)
|
121
|
+
if preview
|
122
|
+
status script
|
123
|
+
else
|
124
|
+
print_output script
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def send_interactive_command command, *args
|
129
|
+
__send__("interactive_#{command}".to_sym, *args)
|
130
|
+
end
|
131
|
+
|
132
|
+
def interactive_abort
|
133
|
+
die! :user_abort
|
134
|
+
end
|
135
|
+
|
136
|
+
def interactive_quit script = @operations
|
137
|
+
print_script script, false
|
138
|
+
exit 0
|
139
|
+
end
|
140
|
+
|
141
|
+
def interactive_print_preview
|
142
|
+
print_script(@operations, true)
|
143
|
+
end
|
144
|
+
|
145
|
+
def interactive_print_status
|
146
|
+
if @operations.empty?
|
147
|
+
status :nothing_edited
|
148
|
+
else
|
149
|
+
status "#{@operations.count} operation#{@operations.count != 1 ? 's' : ''}"
|
150
|
+
counted_operations = { :fail => "error", :warn => "warning" }
|
151
|
+
if @options[:verbose]
|
152
|
+
counted_operations = counted_operations.merge(
|
153
|
+
:moved => "move",
|
154
|
+
:renamed => "rename",
|
155
|
+
:rm => "deleted file",
|
156
|
+
:rmdir => "deleted folder",
|
157
|
+
:mk => "new folder"
|
158
|
+
)
|
159
|
+
end
|
160
|
+
if @operations.any? { |op| counted_operations[op.first] }
|
161
|
+
counted_operations.each_pair do |op_key, label|
|
162
|
+
count = @operations.select { |op| op.first == op_key }.count
|
163
|
+
status " #{count} #{label}#{count != 1 ? "s" : ""}" if count > 0
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def interactive_diff # => "(D)iff"
|
170
|
+
if @edited_listing.to_s.strip == ""
|
171
|
+
status "No edited listing"
|
172
|
+
else
|
173
|
+
DTC::Utils::InteractiveEditor::with_temp_file "Original", ".yaml", @listing_text do |original|
|
174
|
+
DTC::Utils::InteractiveEditor::with_temp_file "Edited", ".yaml", @edited_listing do |edited|
|
175
|
+
status DTC::Utils::Exec::sys(*(@options[:diff_command] + [original, edited, {
|
176
|
+
:ignore_exit_code => true,
|
177
|
+
:capture_stdout => true
|
178
|
+
}]))
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def interactive_edit_listing # => "(E)dit listing"
|
185
|
+
edit_listing!
|
186
|
+
end
|
187
|
+
|
188
|
+
def interactive_revert_listing # => "(R)evert listing"
|
189
|
+
@edited_listing = nil
|
190
|
+
@operations = []
|
191
|
+
edit_listing!
|
192
|
+
end
|
193
|
+
|
194
|
+
def interactive_reconfigure
|
195
|
+
new_options = @options.dup
|
196
|
+
OPTIONS_THAT_CANT_CHANGE_DURING_INTERACTIVE_RUN.each { |o| new_options.delete(o) }
|
197
|
+
new_options = DTC::Utils::InteractiveEditor::edit_in_yaml(new_options, @options[:editor])
|
198
|
+
unless new_options.is_a?(Hash)
|
199
|
+
status "Error, configuration must be a hash table, got #{new_options.inspect}" if new_options
|
200
|
+
return
|
201
|
+
end
|
202
|
+
OPTIONS_THAT_CANT_CHANGE_DURING_INTERACTIVE_RUN.each { |o| new_options.delete(o) }
|
203
|
+
if OPTIONS_THAT_AFFECT_SCANNING.any? { |k| new_options[k] != @options[k] }
|
204
|
+
return unless @operations.count == 0 || ask_user(CONFIRM_MENU_OPTIONS_FOR_RESCAN) == :yes
|
205
|
+
@options = @options.merge(new_options)
|
206
|
+
status "Config updated"
|
207
|
+
update_listing!
|
208
|
+
edit_listing!
|
209
|
+
else
|
210
|
+
@options = @options.merge(new_options)
|
211
|
+
update_operations!
|
212
|
+
status "Config updated"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def interactive_main_menu
|
217
|
+
trap "SIGINT" do
|
218
|
+
interactive_abort
|
219
|
+
end
|
220
|
+
while true
|
221
|
+
options = MAIN_MENU_OPTIONS
|
222
|
+
options = options.merge(MAIN_MENU_OPTIONS_WITH_OPERATIONS) unless @operations.empty?
|
223
|
+
cmd = ask_user(options)
|
224
|
+
send_interactive_command cmd
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def interactive_edit_script original = script_for_operations(@operations), script = original
|
229
|
+
while true
|
230
|
+
result = DTC::Utils::InteractiveEditor::edit(script, ".sh", @options[:editor])
|
231
|
+
unless result.strip == "" || result == original
|
232
|
+
script = result
|
233
|
+
result = ask_user MAIN_MENU_OPTIONS_AFTER_SCRIPT_EDIT
|
234
|
+
case result
|
235
|
+
when :quit, :abort
|
236
|
+
send_interactive_command result, result
|
237
|
+
when :edit_script
|
238
|
+
redo
|
239
|
+
when :revert_script
|
240
|
+
script = script_for_operations(@operations)
|
241
|
+
when :return
|
242
|
+
return
|
243
|
+
end
|
244
|
+
else
|
245
|
+
info "Empty/unchanged script"
|
246
|
+
return
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def scan_path_into_listing base_path = @base_path
|
252
|
+
info "Scanning path #{@base_path} at #{DateTime.now}"
|
253
|
+
builder = FlEd::FileListingBuilder.new()
|
254
|
+
DTC::Utils::Visitor::Folder::accept builder, base_path, @options
|
255
|
+
info "\nFinished at #{DateTime.now}"
|
256
|
+
listing = builder.listing
|
257
|
+
die! :nothing_to_list if listing.count.zero?
|
258
|
+
listing
|
259
|
+
end
|
260
|
+
|
261
|
+
def ops_for_edited_listing listing = @listing, listing_text = @listing_text, edited_listing = @edited_listing
|
262
|
+
return [] if edited_listing.to_s.strip == "" || edited_listing == listing_text
|
263
|
+
begin
|
264
|
+
target_listing = FlEd::FileListing.parse(edited_listing)
|
265
|
+
ops = target_listing.operations_from!(listing)
|
266
|
+
rescue RuntimeError => e
|
267
|
+
ops = [[:fail, "Error parsing line: #{e.message}"]]
|
268
|
+
info e.inspect
|
269
|
+
end
|
270
|
+
return [] if ops.empty?
|
271
|
+
return ops if ops.count == 1 && ops[0][0] == :fail
|
272
|
+
@options[:no_pushd] ? ops : ([[:pushd, @base_path]] + ops + [[:popd]])
|
273
|
+
end
|
274
|
+
|
275
|
+
def script_for_operations operations, line_prefix = ""
|
276
|
+
line_prefix + FlEd::operation_list_to_bash(operations).join("\n#{line_prefix}")
|
277
|
+
end
|
65
278
|
|
66
279
|
def self.default_options
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
DEFAULT_SKIP_FOLDERS.map { |e| '\A' + Regexp.escape(e) + '\z' },
|
75
|
-
})
|
280
|
+
base = DEFAULT_OPTIONS || {}
|
281
|
+
if File.readable?(default_options_filename = File.expand_path(DEFAULT_OPTIONS_PATH))
|
282
|
+
data = File.open(default_options_filename) { |file| YAML.load(file) }
|
283
|
+
base = base.merge(data || {})
|
284
|
+
end
|
285
|
+
base[:editor] = DTC::Utils::InteractiveEditor::sensible_editor unless base[:editor]
|
286
|
+
base
|
76
287
|
end
|
77
288
|
|
78
289
|
def parsed_options?
|
@@ -102,10 +313,12 @@ class App
|
|
102
313
|
|
103
314
|
opts.separator ""
|
104
315
|
opts.separator "General"
|
316
|
+
opts.on('-u', '--[no-]interactive', "Offer to continue editing the listing") { |v| options[:interactive] = v }
|
105
317
|
opts.on_tail('-l', '--load PATH.YAML', "Merge in the specified yaml files options") { |file|
|
106
318
|
File.open(file) { |file| options = options.merge(YAML.load(file)) }
|
107
319
|
}
|
108
|
-
opts.on_tail('--options', "Show options as interpreted and exit without doing anything"
|
320
|
+
opts.on_tail('--options', "Show options as interpreted and exit without doing anything",
|
321
|
+
"Example: #{$0} --options > #{File.expand_path(DEFAULT_OPTIONS_PATH)}") { options[:output_options] = true }
|
109
322
|
opts.on('-v', '--verbose', "Display more information about what the tool is doing...") { options[:verbose] = true }
|
110
323
|
opts.on_tail('--version', "Show version of this tool") { output_version ; exit 0 }
|
111
324
|
opts.on_tail('-h', '--help', "Show this help message") { output_help ; exit 0 }
|
@@ -117,36 +330,111 @@ class App
|
|
117
330
|
true
|
118
331
|
end
|
119
332
|
|
120
|
-
def output_options
|
333
|
+
def output_options
|
121
334
|
opts = @options.dup
|
122
335
|
opts.delete(:output_options)
|
123
|
-
|
336
|
+
print_output opts.to_yaml
|
124
337
|
end
|
125
338
|
|
126
339
|
def output_help
|
127
340
|
output_version
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
341
|
+
status DTC::Utils::Text::lines_without_indent <<-HELP_TEXT
|
342
|
+
|
343
|
+
Disclaimer: The author is not responsible for anything related
|
344
|
+
to this tool. This is quite powerful and slight mistakes can
|
345
|
+
lead to loss of data or worse. The author recommends you not
|
346
|
+
use this.
|
347
|
+
|
348
|
+
Operation:
|
349
|
+
- Generate list of files and folder
|
350
|
+
- Open in your favorite ($EDITOR) text editor
|
351
|
+
- You edit file names in the editor, then save and close
|
352
|
+
- The file list is reloaded and compared to the original
|
353
|
+
- A shell script to re-organise the files/folders is printed
|
354
|
+
|
355
|
+
HELP_TEXT
|
141
356
|
output_usage
|
142
357
|
end
|
143
358
|
|
144
359
|
def output_usage
|
145
|
-
|
360
|
+
status @optionparser
|
146
361
|
end
|
147
362
|
|
148
363
|
def output_version
|
149
|
-
|
364
|
+
status "#{File.basename(__FILE__)} version #{FlEd::VERSION}"
|
365
|
+
end
|
366
|
+
|
367
|
+
def print_prompt options
|
368
|
+
status_raw "=> #{options.values.join(", ")} ? "
|
369
|
+
end
|
370
|
+
|
371
|
+
def print_prompt_error options, error
|
372
|
+
status " ! #{error}"
|
373
|
+
print_prompt options
|
374
|
+
end
|
375
|
+
|
376
|
+
def ask_user options = {}
|
377
|
+
keys = {}
|
378
|
+
options.each_pair { |k, v|
|
379
|
+
keys[v =~ /\((.)\)/ && $1.downcase] = k
|
380
|
+
}
|
381
|
+
print_prompt options
|
382
|
+
until keys[(response_key = readline.downcase.strip)]
|
383
|
+
if response_key == ""
|
384
|
+
message = "please enter one of: #{keys.keys.sort.map(&:inspect).join(", ")}"
|
385
|
+
else
|
386
|
+
message = "didn't understand #{response_key.inspect}"
|
387
|
+
end
|
388
|
+
print_prompt_error options, message
|
389
|
+
end
|
390
|
+
keys[response_key]
|
391
|
+
end
|
392
|
+
|
393
|
+
def readline
|
394
|
+
result = $stdin.readline.chomp
|
395
|
+
@on_incomplete_line = false
|
396
|
+
result
|
397
|
+
end
|
398
|
+
|
399
|
+
def status_raw message
|
400
|
+
$stderr.print message
|
401
|
+
$stderr.flush
|
402
|
+
@on_incomplete_line = !(message.length > 0 && message[-1..-1] == "\n")
|
403
|
+
end
|
404
|
+
|
405
|
+
def print_output message
|
406
|
+
if @on_incomplete_line
|
407
|
+
$stderr.puts ""
|
408
|
+
@on_incomplete_line = false
|
409
|
+
end
|
410
|
+
$stdout.puts message
|
411
|
+
end
|
412
|
+
|
413
|
+
def status message
|
414
|
+
if @on_incomplete_line
|
415
|
+
$stderr.puts ""
|
416
|
+
@on_incomplete_line = false
|
417
|
+
end
|
418
|
+
message = EXIT_REASONS[message][0] if message.is_a?(Symbol)
|
419
|
+
$stderr.puts message
|
420
|
+
end
|
421
|
+
|
422
|
+
def info message
|
423
|
+
status message if @options[:verbose]
|
424
|
+
end
|
425
|
+
|
426
|
+
def die! reason, suffix_text = nil
|
427
|
+
exit_code = 1
|
428
|
+
print_usage = false
|
429
|
+
if (message = EXIT_REASONS[reason])
|
430
|
+
print_usage = message[2] if message[2]
|
431
|
+
exit_code = message[1] if message[1]
|
432
|
+
reason = message[0]
|
433
|
+
end
|
434
|
+
reason = "#{reason}: #{suffix_text}" if suffix_text
|
435
|
+
status reason
|
436
|
+
output_usage if print_usage
|
437
|
+
exit exit_code
|
150
438
|
end
|
151
439
|
end
|
152
440
|
App.new.run
|
data/lib/dtc/utils/exec.rb
CHANGED
@@ -24,8 +24,19 @@ module DTC
|
|
24
24
|
class Exec
|
25
25
|
class << self
|
26
26
|
def sys *opts
|
27
|
-
|
28
|
-
|
27
|
+
options = {}
|
28
|
+
if opts.last.is_a?(Hash)
|
29
|
+
options = opts.pop
|
30
|
+
end
|
31
|
+
arguments = Shellwords.join(opts.flatten.map(&:to_s).to_a)
|
32
|
+
if options[:capture_stdout]
|
33
|
+
result = `#{arguments}`
|
34
|
+
else
|
35
|
+
system(arguments)
|
36
|
+
result = $?.exitstatus
|
37
|
+
end
|
38
|
+
raise "External command error: #{arguments}" unless options[:ignore_exit_code] || $?.success?
|
39
|
+
result
|
29
40
|
end
|
30
41
|
def sys_in cwd, *opts
|
31
42
|
Dir.chdir cwd { sys(*opts) }
|
@@ -43,6 +43,16 @@ module DTC::Utils
|
|
43
43
|
def edit_file_interactively(filename)
|
44
44
|
Exec.sys(@editor, filename)
|
45
45
|
end
|
46
|
+
def self.with_temp_file base_name, extension, content, &block # :yields: temp_filename
|
47
|
+
file = Tempfile.new([base_name, extension])
|
48
|
+
begin
|
49
|
+
file << content
|
50
|
+
file.close
|
51
|
+
yield file.path
|
52
|
+
ensure
|
53
|
+
file.unlink
|
54
|
+
end
|
55
|
+
end
|
46
56
|
def edit_interactively(content)
|
47
57
|
unless @file
|
48
58
|
@file = Tempfile.new(["#{File.basename(__FILE__, File.extname(__FILE__))}-edit", @extension])
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
module DTC
|
3
|
+
module Utils
|
4
|
+
module Meta
|
5
|
+
# Undefines all methods except those
|
6
|
+
# provided as arguments, or if empty,
|
7
|
+
# `class`.
|
8
|
+
#
|
9
|
+
# To undefine even class, specify as
|
10
|
+
# argument: `:no_really_i_dont_even_want_class`
|
11
|
+
def blank_class *exceptions
|
12
|
+
exceptions = exceptions.flatten.map(&:to_sym)
|
13
|
+
exceptions = [:class] if exceptions.empty? && exceptions != [:no_really_i_dont_even_want_class]
|
14
|
+
instance_methods.each { |meth| undef_method(meth) unless meth =~ /\A__/ || meth == :object_id || exceptions.index(meth.to_sym) }
|
15
|
+
end
|
16
|
+
# Replaces provided instance methods,
|
17
|
+
# specified by name or regexp, with
|
18
|
+
# a call that provides the original
|
19
|
+
# method as an argument.
|
20
|
+
#
|
21
|
+
# To define a pass-through method, use:
|
22
|
+
# class Test
|
23
|
+
# extend DTC::Utils::Meta
|
24
|
+
# def test
|
25
|
+
# end
|
26
|
+
# advise :test do |original, *args, &blk|
|
27
|
+
# original.call(*args, &blk)
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
def advise *method_names, &block
|
31
|
+
method_names = method_names.map { |e| e.is_a?(Regexp) ? instance_methods.select { |m| m =~ e } : e }
|
32
|
+
method_names.flatten.each { |e| advise_method(e.to_sym, &block) }
|
33
|
+
end
|
34
|
+
protected
|
35
|
+
def advise_method method_name, &block
|
36
|
+
method_name = method_name.to_sym
|
37
|
+
method = self.instance_method(method_name)
|
38
|
+
define_method(method_name) do |*a, &b|
|
39
|
+
original = lambda { |*args, &sub|
|
40
|
+
method.bind(self).call(*args, &sub)
|
41
|
+
}
|
42
|
+
block.call(original, *a, &b)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module DTC
|
4
|
+
module Utils
|
5
|
+
module Text
|
6
|
+
module HTML
|
7
|
+
class Writer < DTC::Utils::Text::LineWriter
|
8
|
+
def enter sym, *args
|
9
|
+
attrs = args.last.is_a?(Hash) ? args.pop : {}
|
10
|
+
no_indent = attrs && attrs.delete(:no_indent) {
|
11
|
+
! NOINDENT_TAGS.index(sym.to_s.downcase).nil?
|
12
|
+
}
|
13
|
+
push DTC::Utils::Text::HTML::tag(sym, :open, attrs)
|
14
|
+
(@stack ||= []) << sym.to_s.split(".").first
|
15
|
+
push_indent(" " + (current_indent || []).join(""))
|
16
|
+
if no_indent
|
17
|
+
unindented *args
|
18
|
+
else
|
19
|
+
text *args
|
20
|
+
end
|
21
|
+
true
|
22
|
+
end
|
23
|
+
def leave
|
24
|
+
pop_indent
|
25
|
+
push(DTC::Utils::Text::HTML::tag(@stack.pop.to_sym, :close))
|
26
|
+
end
|
27
|
+
def add sym, *args
|
28
|
+
if self.respond_to?(sym)
|
29
|
+
self.__send__(sym, *args)
|
30
|
+
else
|
31
|
+
attrs = args.last.is_a?(Hash) ? args.pop : {}
|
32
|
+
if attrs && attrs.delete(:no_indent) {
|
33
|
+
! NOINDENT_TAGS.index(sym.to_s.downcase).nil?
|
34
|
+
}
|
35
|
+
push(DTC::Utils::Text::HTML::tag(sym, :open, attrs))
|
36
|
+
unindented *args
|
37
|
+
push(DTC::Utils::Text::HTML::tag(sym, :close, attrs))
|
38
|
+
else
|
39
|
+
push(DTC::Utils::Text::HTML::tag(sym, args, attrs))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
def text *str
|
44
|
+
push(*str.flatten.map { |s| CGI::escapeHTML(s) })
|
45
|
+
end
|
46
|
+
def unindented *str
|
47
|
+
push_indent("") { text *str }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
def self.attributes attrs = {}
|
51
|
+
unless attrs.nil? || attrs.empty?
|
52
|
+
" " + (
|
53
|
+
attrs.map do |k, v|
|
54
|
+
key = (
|
55
|
+
ATTRIBUTE_ALIASES[k] ||
|
56
|
+
ATTRIBUTE_ALIASES[k.to_s] ||
|
57
|
+
k
|
58
|
+
).to_s
|
59
|
+
if v == true
|
60
|
+
CGI::escapeHTML(key)
|
61
|
+
elsif !v
|
62
|
+
nil
|
63
|
+
else
|
64
|
+
"#{CGI::escapeHTML(key)}='#{CGI::escapeHTML(v.is_a?(Array) ? v.join(" ") : v.to_s)}'"
|
65
|
+
end
|
66
|
+
end).select {|e| e} .join(" ")
|
67
|
+
else
|
68
|
+
""
|
69
|
+
end
|
70
|
+
end
|
71
|
+
ATTRIBUTE_ALIASES = ({})
|
72
|
+
SHORTFORM_TAGS = %w[area base basefont br col frame hr img input link meta param]
|
73
|
+
NOINDENT_TAGS = %w[pre textarea]
|
74
|
+
def self.tag sym, content = :open_close, attrs = {}
|
75
|
+
tag = sym.to_s.split(".")
|
76
|
+
nature = :open_close
|
77
|
+
content = content.join("") if content.is_a?(Array)
|
78
|
+
if content.is_a?(Symbol)
|
79
|
+
nature = content unless content == :full
|
80
|
+
elsif !content.nil? && content.strip != ""
|
81
|
+
nature = :full
|
82
|
+
else
|
83
|
+
nature = SHORTFORM_TAGS.index(tag.first.downcase) ? :short : :open_close
|
84
|
+
end
|
85
|
+
tag_header = tag_name = tag.first.to_s
|
86
|
+
unless (nature = nature.to_sym) == :close
|
87
|
+
if tag.count > 1
|
88
|
+
attrs ||= {}
|
89
|
+
classes = attrs[:class] || []
|
90
|
+
classes = classes.split(/\s+/) if classes.is_a?(String)
|
91
|
+
classes += tag.drop(1).to_a
|
92
|
+
attrs[:class] = classes.uniq
|
93
|
+
end
|
94
|
+
tag_header += attributes(attrs) unless nature == :close
|
95
|
+
end
|
96
|
+
natures = ({:open => %w[< >], :close => ['</', '>'], :short => ['<', ' />'], :open_close => ['<', "></#{tag_name}>"]})
|
97
|
+
if nature == :full
|
98
|
+
natures[:open].join(tag_header) + content + natures[:close].join(tag_name)
|
99
|
+
else
|
100
|
+
raise RuntimeError, "Unknown tag nature #{nature.inspect}, should be one of #{natures.keys.to_a.inspect}" unless natures[nature]
|
101
|
+
natures[nature].join(tag_header)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|