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/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
- $stderr.puts "Starting on path #{@base_path} at #{DateTime.now}" if @options[:verbose]
34
- builder = FlEd::ListingBuilder.new()
35
- filter = DTC::Utils::FilteringFileVisitor.new builder, @options
36
- DTC::Utils::FileVisitor.browse @base_path, filter
37
- content = builder.listing.to_s
38
- if content == ""
39
- $stderr.puts "No files/folders found"
40
- exit 1
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 Exception => e
60
- puts e
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
- :max_depth => -1,
69
- :excluded_files =>
70
- DEFAULT_SKIP_FILES_RX +
71
- DEFAULT_SKIP_FILES.map { |e| '\A' + Regexp.escape(e) + '\z' },
72
- :excluded_directories =>
73
- DEFAULT_SKIP_FOLDERS_RX +
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") { options[:output_options] = true }
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 out = $stdout
333
+ def output_options
121
334
  opts = @options.dup
122
335
  opts.delete(:output_options)
123
- out.puts opts.to_yaml
336
+ print_output opts.to_yaml
124
337
  end
125
338
 
126
339
  def output_help
127
340
  output_version
128
- $stderr.puts ""
129
- $stderr.puts " Disclaimer: The author is not responsible for anything related"
130
- $stderr.puts " to this tool. This is quite powerful and slight mistakes can"
131
- $stderr.puts " lead to loss of data or worse. The author recommends you not"
132
- $stderr.puts " use this."
133
- $stderr.puts ""
134
- $stderr.puts " Operation:"
135
- $stderr.puts " - Generate list of files and folder"
136
- $stderr.puts " - Open in your favorite ($EDITOR) text editor"
137
- $stderr.puts " - You edit file names in the editor, then save and close"
138
- $stderr.puts " - The file list is reloaded and compared to the original"
139
- $stderr.puts " - A shell script to re-organise the files/folders is printed"
140
- $stderr.puts ""
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
- $stderr.puts @optionparser
360
+ status @optionparser
146
361
  end
147
362
 
148
363
  def output_version
149
- $stderr.puts "#{File.basename(__FILE__)} version #{FlEd::VERSION}"
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
@@ -24,8 +24,19 @@ module DTC
24
24
  class Exec
25
25
  class << self
26
26
  def sys *opts
27
- system(opts.flatten.map {|e| Shellwords::shellescape(e.to_s)}.join(" "))
28
- raise "External command error" unless $?.success?
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