sublime_dsl 0.1.1

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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +136 -0
  3. data/Rakefile +248 -0
  4. data/SYNTAX.md +927 -0
  5. data/bin/subdsl +4 -0
  6. data/lib/sublime_dsl/cli/export.rb +134 -0
  7. data/lib/sublime_dsl/cli/import.rb +143 -0
  8. data/lib/sublime_dsl/cli.rb +125 -0
  9. data/lib/sublime_dsl/core_ext/enumerable.rb +24 -0
  10. data/lib/sublime_dsl/core_ext/string.rb +129 -0
  11. data/lib/sublime_dsl/core_ext.rb +4 -0
  12. data/lib/sublime_dsl/sublime_text/command.rb +157 -0
  13. data/lib/sublime_dsl/sublime_text/command_set.rb +112 -0
  14. data/lib/sublime_dsl/sublime_text/keyboard.rb +659 -0
  15. data/lib/sublime_dsl/sublime_text/keymap/dsl_reader.rb +194 -0
  16. data/lib/sublime_dsl/sublime_text/keymap.rb +385 -0
  17. data/lib/sublime_dsl/sublime_text/macro.rb +91 -0
  18. data/lib/sublime_dsl/sublime_text/menu.rb +237 -0
  19. data/lib/sublime_dsl/sublime_text/mouse.rb +149 -0
  20. data/lib/sublime_dsl/sublime_text/mousemap.rb +185 -0
  21. data/lib/sublime_dsl/sublime_text/package/dsl_reader.rb +91 -0
  22. data/lib/sublime_dsl/sublime_text/package/exporter.rb +138 -0
  23. data/lib/sublime_dsl/sublime_text/package/importer.rb +127 -0
  24. data/lib/sublime_dsl/sublime_text/package/reader.rb +102 -0
  25. data/lib/sublime_dsl/sublime_text/package/writer.rb +112 -0
  26. data/lib/sublime_dsl/sublime_text/package.rb +96 -0
  27. data/lib/sublime_dsl/sublime_text/setting_set.rb +123 -0
  28. data/lib/sublime_dsl/sublime_text.rb +48 -0
  29. data/lib/sublime_dsl/textmate/custom_base_name.rb +45 -0
  30. data/lib/sublime_dsl/textmate/grammar/dsl_reader.rb +383 -0
  31. data/lib/sublime_dsl/textmate/grammar/dsl_writer.rb +178 -0
  32. data/lib/sublime_dsl/textmate/grammar/plist_reader.rb +163 -0
  33. data/lib/sublime_dsl/textmate/grammar/plist_writer.rb +153 -0
  34. data/lib/sublime_dsl/textmate/grammar.rb +252 -0
  35. data/lib/sublime_dsl/textmate/plist.rb +141 -0
  36. data/lib/sublime_dsl/textmate/preference.rb +301 -0
  37. data/lib/sublime_dsl/textmate/snippet.rb +437 -0
  38. data/lib/sublime_dsl/textmate/theme/dsl_reader.rb +87 -0
  39. data/lib/sublime_dsl/textmate/theme/item.rb +74 -0
  40. data/lib/sublime_dsl/textmate/theme/plist_writer.rb +53 -0
  41. data/lib/sublime_dsl/textmate/theme.rb +364 -0
  42. data/lib/sublime_dsl/textmate.rb +9 -0
  43. data/lib/sublime_dsl/tools/blank_slate.rb +49 -0
  44. data/lib/sublime_dsl/tools/console.rb +74 -0
  45. data/lib/sublime_dsl/tools/helpers.rb +152 -0
  46. data/lib/sublime_dsl/tools/regexp_wannabe.rb +154 -0
  47. data/lib/sublime_dsl/tools/stable_inspect.rb +20 -0
  48. data/lib/sublime_dsl/tools/value_equality.rb +37 -0
  49. data/lib/sublime_dsl/tools/xml.rb +66 -0
  50. data/lib/sublime_dsl/tools.rb +66 -0
  51. data/lib/sublime_dsl.rb +23 -0
  52. metadata +145 -0
@@ -0,0 +1,138 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+ class Package
6
+
7
+ ##
8
+ # An export operation with its options.
9
+
10
+ class Exporter
11
+
12
+ attr_reader :package
13
+ attr_accessor :root, :backup, :cleanup
14
+
15
+ def initialize(package = nil)
16
+ @package = package
17
+ @root = SublimeText.packages_dir
18
+ @backup = :always
19
+ @cleanup = false
20
+ end
21
+
22
+ def export(package = nil)
23
+ @package = package if package
24
+ validate
25
+ perform_backup
26
+ Console.info "exporting package to #{dir}"
27
+ Dir.mktmpdir('sublime_dsl_') do |tmpdir|
28
+ tmpdir = tmpdir.encode('utf-8') # for ruby 2.0
29
+ export_package tmpdir
30
+ copy_package tmpdir, dir
31
+ end
32
+ end
33
+
34
+ def dir
35
+ "#{root}/#{@package.name}"
36
+ end
37
+
38
+ def keyboard=(name)
39
+ @keyboard_name = name
40
+ end
41
+
42
+ def validate
43
+ File.directory?(root) or raise OptionError, "no such directory: #{root}"
44
+ @backup = backup.to_sym
45
+ unless [:never, :always, :once].include?(backup)
46
+ warn "invalid backup option #{backup.inspect}, 'always' assumed"
47
+ @backup = :always
48
+ end
49
+ end
50
+
51
+ def perform_backup
52
+ # no backup needed if not asked or no file to backup
53
+ return unless backup != :never && Dir["#{dir}/*"].length > 0
54
+ # don't do it if only one backup is requested, and there is already one
55
+ name = Tools.filename(package.name)
56
+ return if backup == :once && Dir["#{root}/#{name}.*.zip"].length > 0
57
+ # backup
58
+ time_stamp = Tools.filename(Time.now.strftime("%Y-%m-%d %H:%M:%S"))
59
+ backup_file = "#{root}/#{name}.#{time_stamp}.zip"
60
+ Tools.zip dir, backup_file
61
+ Console.info "#{dir} backed up as #{backup_file}"
62
+ end
63
+
64
+ def export_package(dir)
65
+ p = package
66
+ p.themes.each { |t| t.export dir }
67
+ p.grammars.each { |g| g.export dir }
68
+ p.preferences.each { |pr| pr.export dir }
69
+ p.snippets.each { |s| s.export dir }
70
+ p.setting_sets.each { |s| s.export dir }
71
+ p.macros.each { |m| m.export dir }
72
+ p.command_sets.each { |c| c.export dir }
73
+ p.menus.each { |m| m.export dir }
74
+ p.mousemaps.each { |m| m.export dir }
75
+ p.keymaps.each { |k| k.export dir }
76
+ p.other_files.each { |f| copy_file f, dir }
77
+ end
78
+
79
+ def copy_package(from, to)
80
+
81
+ # remember the files originally there
82
+ if File.directory?(to)
83
+ old_files = Dir["#{to}/**/*"]
84
+ else
85
+ Dir.mkdir to
86
+ old_files = []
87
+ end
88
+
89
+ # copy from the temporary directory
90
+ new_files = []
91
+ Dir["#{from}/**/*"].each do |fromfile|
92
+ tofile = fromfile.dup
93
+ tofile[from] = to
94
+ new_files << tofile
95
+ if File.directory?(fromfile)
96
+ Dir.mkdir tofile unless File.directory?(tofile)
97
+ else
98
+ FileUtils.cp fromfile, tofile
99
+ end
100
+ end
101
+
102
+ return unless cleanup
103
+
104
+ # on Windows, do not delete files that had just case changes
105
+ if Tools.os == :Windows
106
+ old_files = old_files.map(&:downcase)
107
+ new_files = new_files.map(&:downcase)
108
+ end
109
+
110
+ # delete files/directories that disappeared
111
+ (old_files - new_files).each do |f|
112
+ next unless File.exist?(f) # removing directories also remove files
113
+ if File.directory?(f)
114
+ FileUtils.rm_r [f]
115
+ else
116
+ FileUtils.rm f
117
+ end
118
+ end
119
+
120
+ end
121
+
122
+ def copy_file(infile, outdir)
123
+ if File.directory?(infile)
124
+ outpath = "#{outdir}/#{File.basename(infile)}"
125
+ Dir.mkdir outpath unless File.directory?(outpath)
126
+ Dir["#{infile}/*"].each { |f| copy_file f, outpath }
127
+ else
128
+ # Console.progress ...
129
+ FileUtils.cp infile, outdir
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,127 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+ class Package
6
+
7
+ ##
8
+ # An import operation with its options.
9
+
10
+ class Importer
11
+
12
+ attr_reader :package_name
13
+ attr_accessor :root, :alt_root, :include, :exclude
14
+
15
+ def initialize(name)
16
+ @package_name = name
17
+ @root = SublimeText.packages_dir
18
+ @alt_root = nil
19
+ @include = nil
20
+ @exclude = '*.cache;*.pyc'
21
+ end
22
+
23
+ def dir
24
+ "#{root}/#{package_name}"
25
+ end
26
+
27
+ def package
28
+ @package ||= import
29
+ end
30
+
31
+ private
32
+
33
+ def validate
34
+ File.directory?(dir) or raise OptionError, "no such directory: #{dir}"
35
+ alt_root.nil? || File.directory?(alt_root) or
36
+ raise OptionError, "no such directory: #{alt_root}"
37
+ end
38
+
39
+ def files
40
+ @files ||= begin
41
+ files = Dir["#{dir}/*"]
42
+
43
+ if self.include
44
+ specs = self.include.split(';')
45
+ files.select! do |f|
46
+ f = File.basename(f)
47
+ specs.any? { |s| File.fnmatch(s, f) }
48
+ end
49
+ end
50
+
51
+ if self.exclude
52
+ specs = self.exclude.split(';')
53
+ files.reject! do |f|
54
+ f = File.basename(f)
55
+ specs.any? { |s| File.fnmatch(s, f) }
56
+ end
57
+ end
58
+
59
+ files
60
+ end
61
+ end
62
+
63
+ def import
64
+ validate
65
+
66
+ if files.empty?
67
+ Console.info "#{dir}: nothing to import"
68
+ return nil
69
+ end
70
+
71
+ Console.info "importing package from #{dir}"
72
+ package = Package.new(package_name)
73
+ files.each.with_index(1) do |file, index|
74
+ file = selected_file(file)
75
+ Console.progress index, files.length, file
76
+ begin
77
+ case File.extname(file).downcase
78
+ when '.tmtheme'
79
+ package.themes << TextMate::Theme.import(file)
80
+ when '.tmlanguage'
81
+ package.grammars << TextMate::Grammar.import(file)
82
+ when '.tmpreferences'
83
+ package.preferences << TextMate::Preference.import(file)
84
+ when '.tmsnippet', '.sublime-snippet'
85
+ package.snippets << TextMate::Snippet.import(file)
86
+ when '.sublime-settings'
87
+ package.setting_sets << SettingSet.import(file)
88
+ when '.sublime-macro'
89
+ package.macros << Macro.import(file)
90
+ when '.sublime-commands'
91
+ package.command_sets << CommandSet.import(file)
92
+ when '.sublime-menu'
93
+ package.menus << Menu.import(file)
94
+ when '.sublime-mousemap'
95
+ package.mousemaps << MouseMap.import(file)
96
+ when '.sublime-keymap'
97
+ package.keymaps << KeyMap.import(file)
98
+
99
+ when '.sublime-build'
100
+ # not worth it
101
+ package.other_files << file
102
+ when '.sublime-completions'
103
+ # only HTML & PHP, not surviving in ST 3
104
+ package.other_files << file
105
+ else
106
+ package.other_files << file
107
+ end
108
+ rescue => ex
109
+ Console.error "error when importing #{file}:"
110
+ raise ex
111
+ end
112
+ end
113
+
114
+ package
115
+ end
116
+
117
+ def selected_file(file)
118
+ return file unless alt_root
119
+ alt_file = file.sub(root, alt_root)
120
+ File.exist?(alt_file) ? alt_file : file
121
+ end
122
+
123
+ end
124
+
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,102 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+ class Package
6
+
7
+ ##
8
+ # A DSL read operation with its options.
9
+
10
+ class Reader
11
+
12
+ attr_reader :package_name
13
+ attr_accessor :root, :alt_root, :include, :exclude
14
+
15
+ def initialize(name)
16
+ @package_name = name
17
+ @root = '.'
18
+ @alt_root = nil
19
+ @include = nil
20
+ @exclude = '*.keyboard.rb'
21
+ end
22
+
23
+ def dir
24
+ "#{root}/#{package_name}"
25
+ end
26
+
27
+ def package
28
+ @package ||= read
29
+ end
30
+
31
+ private
32
+
33
+ def validate
34
+ File.directory?(dir) or raise OptionError, "no such directory: #{dir}"
35
+ alt_root.nil? || File.directory?(alt_root) or
36
+ raise OptionError, "no such directory: #{alt_root}"
37
+ end
38
+
39
+ def files
40
+ @files ||= begin
41
+ files = Dir["#{dir}/*"]
42
+
43
+ if self.include
44
+ specs = self.include.split(';')
45
+ files.select! do |f|
46
+ f = File.basename(f)
47
+ specs.any? { |s| File.fnmatch(s, f) }
48
+ end
49
+ end
50
+
51
+ if self.exclude
52
+ specs = self.exclude.split(';')
53
+ files.reject! do |f|
54
+ f = File.basename(f)
55
+ specs.any? { |s| File.fnmatch(s, f) }
56
+ end
57
+ end
58
+
59
+ files
60
+ end
61
+ end
62
+
63
+ def read
64
+ validate
65
+
66
+ if files.empty?
67
+ Console.info "#{dir}: nothing to read"
68
+ return nil
69
+ end
70
+
71
+ Console.info "processing DSL files in #{dir}"
72
+ package = Package.new(package_name)
73
+
74
+ files.each.with_index(1) do |file, index|
75
+ file = selected_file(file)
76
+ Console.progress index, files.length, file
77
+ begin
78
+ if File.basename(file) =~ /\.rb$/
79
+ DSLReader.new file, package
80
+ else
81
+ package.other_files << file
82
+ end
83
+ rescue => ex
84
+ Console.error "error when reading #{file}:"
85
+ raise ex
86
+ end
87
+ end
88
+
89
+ package
90
+ end
91
+
92
+ def selected_file(file)
93
+ return file unless alt_root
94
+ alt_file = file.sub(root, alt_root)
95
+ File.exist?(alt_file) ? alt_file : file
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,112 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+ class Package
6
+
7
+ ##
8
+ # A DSL writing operation with its options.
9
+
10
+ class Writer
11
+
12
+ attr_reader :package
13
+ attr_accessor :root, :backup, :copy_other_files
14
+
15
+ def initialize(package = nil)
16
+ @package = package
17
+ @root = '.'
18
+ @backup = :once
19
+ @copy_other_files = true
20
+ @keyboard_name = nil
21
+ end
22
+
23
+ def write(package = nil)
24
+ @package = package if package
25
+ validate
26
+ perform_backup
27
+ FileUtils.rm_r Dir["#{dir}/*"] if File.directory?(dir)
28
+ Console.info "writing package DSL to #{dir}"
29
+ write_package
30
+ end
31
+
32
+ def dir
33
+ "#{root}/#{@package.name}"
34
+ end
35
+
36
+ def keyboard=(name)
37
+ @keyboard_name = name
38
+ end
39
+
40
+ def keyboard
41
+ @keyboard ||=
42
+ if @keyboard_name.nil? || @keyboard_name == Keyboard.sublime.name
43
+ Keyboard.sublime
44
+ else
45
+ Keyboard.get(@keyboard_name, root)
46
+ end
47
+ end
48
+
49
+ def validate
50
+ File.directory?(root) or raise OptionError, "no such directory: #{root}"
51
+ @backup = backup.to_sym
52
+ unless [:never, :always, :once].include?(backup)
53
+ warn "invalid backup option #{backup.inspect}, 'always' assumed"
54
+ @backup = :always
55
+ end
56
+ keyboard
57
+ end
58
+
59
+ def perform_backup
60
+ # no backup needed if not asked or no file to backup
61
+ return unless backup != :never && Dir["#{dir}/*"].length > 0
62
+ # don't do it if only one backup is requested, and there is already one
63
+ name = Tools.filename(package.name)
64
+ return if backup == :once && Dir["#{root}/#{name}.*.zip"].length > 0
65
+ # backup
66
+ time_stamp = Tools.filename(Time.now.strftime("%Y-%m-%d %H:%M:%S"))
67
+ backup_file = "#{root}/#{name}.#{time_stamp}.zip"
68
+ Tools.zip dir, backup_file
69
+ Console.info "#{dir} backed up as #{backup_file}"
70
+ end
71
+
72
+ def write_package
73
+ Dir.mkdir dir unless File.directory?(dir)
74
+ p = package
75
+ p.themes.each { |t| t.write dir }
76
+ p.grammars.each { |g| g.write dir }
77
+ write_all p.preferences, "#{dir}/preferences.rb"
78
+ TextMate::Snippet::DSLWriter.new(p.snippets).write "#{dir}/snippets.rb"
79
+ write_all p.setting_sets, "#{dir}/settings.rb"
80
+ write_all p.macros.sort_by(&:name), "#{dir}/macros.rb"
81
+ write_all p.command_sets, "#{dir}/commands.rb"
82
+ p.menus.each { |m| m.write dir }
83
+ p.mousemaps.each { |m| m.write dir }
84
+ p.keymaps.each { |k| k.for_keyboard(keyboard).write dir }
85
+ return unless copy_other_files
86
+ p.other_files.each { |f| copy_file f, dir }
87
+ end
88
+
89
+ def copy_file(infile, outdir)
90
+ if File.directory?(infile)
91
+ outpath = "#{outdir}/#{File.basename(infile)}"
92
+ Dir.mkdir outpath unless File.directory?(outpath)
93
+ Dir["#{infile}/*"].each { |f| copy_file f, outpath }
94
+ else
95
+ # Console.progress ...
96
+ FileUtils.cp infile, outdir
97
+ end
98
+ end
99
+
100
+ def write_all(collection, file)
101
+ return if collection.empty?
102
+ File.open(file, 'wb:utf-8') do |f|
103
+ f.puts '# encoding: utf-8'
104
+ collection.each { |e| f.puts "\n#{e.to_dsl}" }
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,96 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'package/importer'
4
+ require_relative 'package/writer'
5
+ require_relative 'package/reader'
6
+ require_relative 'package/exporter'
7
+ require_relative 'package/dsl_reader'
8
+
9
+ module SublimeDSL
10
+ module SublimeText
11
+
12
+ class Package
13
+
14
+ # Create a new Package from a Sublime Text 'Packages' subdirectory.
15
+ #
16
+ # The options can be any Importer option.
17
+
18
+ def self.import(name, options = {})
19
+ importer = Importer.new(name)
20
+ process_options importer, options
21
+
22
+ importer.package
23
+ end
24
+
25
+ # Create a new Package from a DSL directory.
26
+ #
27
+ # The options can be any Reader option.
28
+
29
+ def self.read(name, options = {})
30
+ reader = Reader.new(name)
31
+ process_options reader, options
32
+
33
+ reader.package
34
+ end
35
+
36
+ def self.process_options(object, options)
37
+ options.each_pair do |k,v|
38
+ m = "#{k}="
39
+ raise OptionError, "invalid option '#{k}'" unless object.respond_to? m
40
+ object.send m, v
41
+ end
42
+ end
43
+
44
+ attr_reader :name
45
+
46
+ attr_reader :themes
47
+ attr_reader :grammars
48
+ attr_reader :preferences
49
+ attr_reader :snippets
50
+ attr_reader :setting_sets
51
+ attr_reader :macros
52
+ attr_reader :command_sets
53
+ attr_reader :menus
54
+ attr_reader :mousemaps
55
+ attr_reader :keymaps
56
+
57
+ attr_reader :other_files
58
+
59
+ def initialize(name)
60
+ @name = name
61
+ @themes = []
62
+ @grammars = []
63
+ @preferences = []
64
+ @snippets = []
65
+ @setting_sets = []
66
+ @macros = []
67
+ @command_sets = []
68
+ @menus = []
69
+ @mousemaps = []
70
+ @keymaps = []
71
+
72
+ @other_files = []
73
+ end
74
+
75
+ # Writes the package to DSL files.
76
+ # The options can be any Writer option.
77
+
78
+ def write(options = {})
79
+ writer = Writer.new(self)
80
+ self.class.process_options writer, options
81
+ writer.write
82
+ end
83
+
84
+ # Exports the package.
85
+ # The options can be any Exporter option.
86
+
87
+ def export(options = {})
88
+ exporter = Exporter.new(self)
89
+ self.class.process_options exporter, options
90
+ exporter.export
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,123 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+
6
+ class SettingSet
7
+
8
+ def self.import(file)
9
+ name = File.basename(file, File.extname(file))
10
+ set = new(name)
11
+ json = File.read(file, encoding: 'utf-8')
12
+ h = json.empty? ? {} : JSON[json]
13
+ begin
14
+ h.each_pair { |key, value| set.settings << Setting.new(key, value) }
15
+ rescue => ex
16
+ puts "file: #{file}"
17
+ raise ex
18
+ end
19
+
20
+ set
21
+ end
22
+
23
+ attr_reader :name, :settings
24
+
25
+ def initialize(name)
26
+ @name = name
27
+ @settings = []
28
+ end
29
+
30
+ def to_dsl
31
+ dsl = "settings #{name.to_source} do\n"
32
+ dsl << "\n" unless settings.empty?
33
+ settings.each { |s| dsl << " #{s.to_dsl}\n" }
34
+ dsl << "\nend"
35
+ end
36
+
37
+ def export(dir)
38
+ file = "#{dir}/#{name}.sublime-settings"
39
+ File.open(file, 'wb:utf-8') { |f| f.write to_json }
40
+ end
41
+
42
+ def to_json
43
+ h = Hash[settings.map { |s| [s.key, s.value] }]
44
+ JSON.pretty_generate(h)
45
+ end
46
+
47
+ class Setting
48
+
49
+ attr_reader :key, :value
50
+
51
+ def initialize(key, value)
52
+ @key = key
53
+ @value = value
54
+ end
55
+
56
+ def to_dsl
57
+ "#{key} #{display(value)}"
58
+ end
59
+
60
+ private
61
+
62
+ def display(v)
63
+ case v
64
+ when String
65
+ v.to_source
66
+ when TrueClass, FalseClass, Numeric
67
+ v.to_s
68
+ when Array
69
+ display_array(v)
70
+ when Hash
71
+ v.empty? ? 'Hash.new' : v.inspect[1..-2] # remove braces
72
+ when NilClass
73
+ 'nil'
74
+ else
75
+ raise Error, "value: #{v.inspect} for #{name.inspect}"
76
+ end
77
+ end
78
+
79
+ def display_array(a)
80
+ if a.empty?
81
+ '[]'
82
+ elsif a.all? { |e| e.is_a?(String) && e =~ /\A(\S+)\z/ }
83
+ "%w(#{a.join(' ')})"
84
+ else
85
+ a.inspect
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ class DSLReader < Tools::BlankSlate
92
+
93
+ def initialize(file = nil)
94
+ @setting_sets = []
95
+ @current_set = nil
96
+ instance_eval File.read(file, encoding: 'utf-8'), file if file
97
+ end
98
+
99
+ def _setting_sets
100
+ @setting_sets
101
+ end
102
+
103
+ def method_missing(sym, *args, &block)
104
+ @current_set or raise Error, "'#{sym}' is invalid outside of a 'settings' block"
105
+ args.empty? and raise Error, "no value for setting '#{sym}'"
106
+ args.length > 1 and raise Error, "more than one value for setting '#{sym}'"
107
+ @current_set.settings << Setting.new(sym.to_s, args.first)
108
+ end
109
+
110
+ def settings(name, &block)
111
+ @current_set and raise Error, "'settings' blocks cannot be nested"
112
+ @current_set = SettingSet.new(name)
113
+ instance_eval(&block)
114
+ @setting_sets << @current_set
115
+ @current_set = nil
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+ end