sublime_dsl 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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,237 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+
6
+ class Menu
7
+
8
+ def self.import(file)
9
+ name = File.basename(file, File.extname(file))
10
+ set = new(name)
11
+ list = JSON[File.read(file, encoding: 'utf-8')]
12
+ set.items.concat list.map { |h| Item.from_json(h) }
13
+
14
+ set
15
+ end
16
+
17
+ attr_reader :name, :items
18
+
19
+ def initialize(name)
20
+ @name = name
21
+ @items = []
22
+ end
23
+
24
+ def to_dsl
25
+ dsl = "menu #{name.to_source} do\n\n"
26
+ items.each { |i| dsl << "#{i.to_dsl}\n" }
27
+ dsl << "\nend"
28
+ end
29
+
30
+ def write(dir)
31
+ file = "#{dir}/#{name}.menu.rb"
32
+ File.open(file, 'wb:utf-8') do |f|
33
+ f.puts '# encoding: utf-8'
34
+ f.puts "\n#{to_dsl}"
35
+ end
36
+ end
37
+
38
+ def export(dir)
39
+ file = "#{dir}/#{name}.sublime-menu"
40
+ File.open(file, 'wb:utf-8') { |f| f.write to_json }
41
+ end
42
+
43
+ def to_json
44
+ "[\n" <<
45
+ items.map { |i| i.to_json(' ') }.join(",\n") <<
46
+ "\n]"
47
+ end
48
+
49
+
50
+ class Item
51
+
52
+ def self.from_json(json_hash)
53
+ h = json_hash.dup
54
+ item = Item.new.tap do |i|
55
+ i.caption = h.delete('caption')
56
+ i.mnemonic = h.delete('mnemonic')
57
+ cmd = h.delete('command')
58
+ args = h.delete('args')
59
+ i.command = Command.new(cmd, args) if cmd
60
+ i.id = h.delete('id')
61
+ i.checkbox = h.delete('checkbox')
62
+ i.platform = h.delete('platform')
63
+ end
64
+ children = h.delete('children') || []
65
+ children.each do |c|
66
+ item.items << Item.from_json(c)
67
+ end
68
+ h.empty? or warn "unkown keys ignored: #{h.inspect}"
69
+
70
+ item
71
+ end
72
+
73
+ attr_accessor :command, :caption, :mnemonic, :id, :checkbox, :platform
74
+ attr_reader :items
75
+
76
+ def initialize()
77
+ @command = nil
78
+ @caption = nil
79
+ @mnemonic = nil
80
+ @id = nil
81
+ @checkbox = nil
82
+ @platform = nil
83
+ @items = []
84
+ end
85
+
86
+ def to_dsl(indent = ' ')
87
+ args = ''
88
+ options = []
89
+
90
+ if caption
91
+ cap = caption.gsub('&', '&&')
92
+ if mnemonic
93
+ if cap =~ /^(.*?)(#{mnemonic})(.*)$/i
94
+ args << "#{$1}&#{$2}#{$3}".to_source
95
+ else
96
+ args << cap.to_source
97
+ options << "mnemonic: #{mnemonic.to_source}"
98
+ end
99
+ else
100
+ args << cap.to_source
101
+ end
102
+ end
103
+
104
+ if command
105
+ args << ', ' unless args.empty?
106
+ args << command.to_dsl
107
+ options << "mnemonic: #{mnemonic.to_source}" if caption.nil? && mnemonic
108
+ end
109
+
110
+ options << "id: #{id.to_source}" if id
111
+ options << "checkbox: true" if checkbox
112
+ options << "platform: #{platform.to_source}" if platform
113
+
114
+ dsl = "#{indent}item #{args}"
115
+ unless options.empty?
116
+ dsl << ', ' unless args.empty?
117
+ dsl << options.join(', ')
118
+ end
119
+
120
+ unless items.empty?
121
+ i = indent + ' '
122
+ dsl << " do\n"
123
+ items.each do |c|
124
+ dsl << c.to_dsl(i) << "\n"
125
+ end
126
+ dsl << "#{indent}end"
127
+ end
128
+
129
+ dsl
130
+ end
131
+
132
+ def to_h(include_items = true)
133
+ h = {}
134
+ h['caption'] = caption if caption
135
+ h['mnemonic'] = mnemonic if mnemonic
136
+ h.merge! command.to_h if command
137
+ h['id'] = id if id
138
+ h['checkbox'] = checkbox if checkbox
139
+ h['platform'] = platform if platform
140
+ h['children'] = items.map(&:to_h) if include_items && !items.empty?
141
+ h
142
+ end
143
+
144
+ def to_json(indent)
145
+ return indent + JSON.generate(to_h) if items.empty?
146
+ json = indent + JSON.pretty_generate(to_h(false))
147
+ json = json[0..-3] # remove trailing "\n}"
148
+ json.gsub!("\n", "\n#{indent}")
149
+ json << %(,\n#{indent} "children": [\n)
150
+ ind = indent + ' '
151
+ json << items.map { |i| i.to_json(ind) }.join(",\n")
152
+ json << "\n#{indent} ]\n#{indent}}"
153
+ end
154
+
155
+ end
156
+
157
+
158
+ class DSLReader < Tools::BlankSlate
159
+
160
+ def initialize(file = nil)
161
+ @menus = []
162
+ @item_stack = []
163
+ instance_eval File.read(file, encoding: 'utf-8'), file if file
164
+ end
165
+
166
+ def _menus
167
+ @menus
168
+ end
169
+
170
+ def menu(name, &block)
171
+ @item_stack.empty? or raise Error, "menu blocks cannot be nested"
172
+ @item_stack.push Menu.new(name)
173
+ instance_eval(&block)
174
+ @menus << @item_stack.pop
175
+ end
176
+
177
+ def item(*args, &block)
178
+ @item_stack.empty? and raise Error, "'item' is invalid outside of a menu block"
179
+ item = new_item(args)
180
+ @item_stack.last.items << item
181
+ return unless block
182
+ @item_stack.push item
183
+ instance_eval(&block)
184
+ @item_stack.pop
185
+ end
186
+
187
+ def method_missing(sym, *args, &block)
188
+ @item_stack.empty? and raise Error, "'#{sym}' is invalid outside of a menu block"
189
+ Command.from_method_missing(sym, args)
190
+ end
191
+
192
+ def new_item(args)
193
+ args.empty? and raise Error, "no argument for 'item'"
194
+ item = Item.new
195
+
196
+ # get the caption if any
197
+ if args.first.is_a?(String)
198
+ caption = args.shift
199
+ caption =~ /&([^& ])/ and item.mnemonic = $1.upcase
200
+ item.caption = caption.gsub(/&(.)/, '\1')
201
+ return item if args.empty?
202
+ end
203
+
204
+ # get the command if any
205
+ cmd = args.first
206
+ if cmd.is_a?(Command)
207
+ cmd.error and raise Error, "item '#{caption}': #{cmd.error}"
208
+ item.command = cmd
209
+ args.shift
210
+ return item if args.empty?
211
+ end
212
+
213
+ # options
214
+ options = args.first
215
+ args.length == 1 && options.is_a?(Hash) or
216
+ raise Error, "invalid arguments for 'item': #{args.inspect}"
217
+
218
+ item.id = options.delete(:id)
219
+ item.checkbox = options.delete(:checkbox)
220
+ item.platform = options.delete(:platform)
221
+ mnemonic = options.delete(:mnemonic)
222
+ if mnemonic
223
+ item.mnemonic && mnemonic != item.mnemonic and
224
+ warn "item '#{caption}': mnemonic #{item.mnemonic} overwritten by #{mnemonic}"
225
+ item.mnemonic = mnemonic
226
+ end
227
+
228
+ item
229
+ end
230
+
231
+ end
232
+
233
+
234
+ end
235
+
236
+ end
237
+ end
@@ -0,0 +1,149 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+
6
+ class Mouse
7
+
8
+ def self.sublime
9
+ @sublime ||= create_sublime_mouse
10
+ end
11
+
12
+ def self.create_sublime_mouse
13
+ mouse = new('Sublime Text')
14
+ (1..9).each { |i| mouse.add_button "button#{i}" }
15
+ mouse.add_button "scroll_up"
16
+ mouse.add_button "scroll_down"
17
+ %w(shift ctrl alt super).each { |m| mouse.add_modifier m }
18
+ mouse.modifiers.concat mouse.buttons
19
+ mouse
20
+ end
21
+
22
+ attr_reader :name, :buttons, :modifiers
23
+
24
+ def initialize(name)
25
+ @name = name
26
+ @buttons = []
27
+ @modifiers = []
28
+ @clicks = []
29
+ end
30
+
31
+ alias to_s name
32
+
33
+ def clone(name)
34
+ clone = Mouse.new(name)
35
+ buttons.each { |b| clone.add_button b.st_name, b.name }
36
+ (modifiers - buttons).each { |m| clone.add_modifier m.st_name, m.name }
37
+ clone.modifiers.concat clone.buttons
38
+ clone
39
+ end
40
+
41
+ def button(name)
42
+ buttons.find { |b| b.name == name }
43
+ end
44
+
45
+ def modifier(name)
46
+ modifiers.find { |b| b.name == name }
47
+ end
48
+
49
+ def add_button(st_name, name = nil)
50
+ @buttons << Button.new(st_name, name)
51
+ end
52
+
53
+ def add_modifier(st_name, name = nil)
54
+ @modifiers << Button.new(st_name, name)
55
+ end
56
+
57
+ def clicks_hash
58
+ @clicks_hash ||= {}
59
+ end
60
+
61
+ def modifiers_index_hash
62
+ @modifiers_index_hash ||= begin
63
+ h = {}
64
+ modifiers.each_with_index { |b,i| h[b.name] = i }
65
+ h
66
+ end
67
+ end
68
+
69
+ def ensure_click(spec)
70
+ *modifier_names, button_name = spec.split('+')
71
+
72
+ button = button(button_name) or
73
+ raise Error, "invalid button #{button_name.inspect}"
74
+
75
+ # check & reorder the modifiers
76
+ unless modifier_names.empty?
77
+ sorted = []
78
+ modifier_names.each do |name|
79
+ i = modifiers_index_hash[name]
80
+ i or raise Error, "invalid modifier #{name.inspect}"
81
+ sorted[i] = name
82
+ end
83
+ modifier_names = sorted.compact
84
+ end
85
+
86
+ # if there is a registered click for this spec, return it
87
+ std_spec = [*modifier_names, button_name].join('+')
88
+ click = clicks_hash[std_spec]
89
+ return click if click
90
+
91
+ # can't have the button in the modifiers
92
+ modifier_names.include?(button_name) and
93
+ raise Error, "can't have the button in the modifiers: #{spec.inspect}"
94
+
95
+ # create and register the click
96
+ modifiers = modifier_names.map { |n| modifier(n) }
97
+ click = Click.new(modifiers, button)
98
+ clicks_hash[click.to_spec] = click
99
+
100
+ click
101
+ end
102
+
103
+
104
+ ##
105
+ # A mouse button or modifier.
106
+
107
+ class Button
108
+
109
+ attr_reader :st_name
110
+ attr_accessor :name
111
+
112
+ def initialize(st_name, name)
113
+ @st_name = st_name
114
+ @name = name || st_name
115
+ end
116
+
117
+ end
118
+
119
+
120
+ ##
121
+ # A mouse click: button + modifiers.
122
+
123
+ class Click
124
+
125
+ attr_reader :button, :modifiers
126
+
127
+ def initialize(modifiers, button)
128
+ @modifiers = modifiers
129
+ @button = button
130
+ end
131
+
132
+ def to_spec
133
+ [*modifiers.map(&:name), button.name].join('+')
134
+ end
135
+
136
+ alias to_s to_spec
137
+
138
+ def to_h
139
+ h = { 'button' => button.st_name }
140
+ h['modifiers'] = modifiers.map(&:st_name) unless modifiers.empty?
141
+ h
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,185 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+
6
+ class MouseMap
7
+
8
+ def self.import(file)
9
+ name = File.basename(file, File.extname(file))
10
+ map = new(name)
11
+ list = JSON[File.read(file, encoding: 'utf-8')]
12
+ begin
13
+ map.bindings.concat list.map { |h| MouseBinding.from_json(h) }
14
+ rescue => ex
15
+ puts "file: #{file}"
16
+ raise ex
17
+ end
18
+
19
+ map
20
+ end
21
+
22
+ attr_reader :name, :bindings
23
+
24
+ def initialize(name)
25
+ @name = name
26
+ @bindings = []
27
+ end
28
+
29
+ def write(dir)
30
+ file = "#{dir}/#{name}.mousemap.rb"
31
+ File.open(file, 'wb') do |f|
32
+ f.puts "\nmousemap #{name.to_source} do"
33
+ f.puts
34
+ bindings.each do |b|
35
+ begin
36
+ f.puts ' ' << b.to_dsl
37
+ rescue => ex
38
+ puts "file: #{file}\nbinding: #{b.click}"
39
+ raise ex
40
+ end
41
+ end
42
+ f.puts "\nend"
43
+ end
44
+ end
45
+
46
+ def export(dir)
47
+ file = "#{dir}/#{name}.sublime-mousemap"
48
+ File.open(file, 'wb:utf-8') { |f| f.write to_json }
49
+ end
50
+
51
+ def to_json
52
+ "[\n" <<
53
+ bindings.map { |b| JSON.generate(b.to_h) }.join(",\n").indent(2) <<
54
+ "\n]"
55
+ end
56
+
57
+
58
+ ##
59
+ # A mouse binding: a click with its command(s).
60
+
61
+ class MouseBinding
62
+
63
+ def self.from_json(json_hash)
64
+ h = json_hash.dup
65
+
66
+ button = h.delete('button') or raise Error, 'no button: ' << json_hash.inspect
67
+ modifiers = h.delete('modifiers')
68
+ spec = [*modifiers, button].join('+')
69
+ click = Mouse.sublime.ensure_click(spec)
70
+
71
+ count = h.delete('count')
72
+ count = count.to_i if count
73
+
74
+ press = h.delete('press_command')
75
+ press_command = press ? Command.new(press, h.delete('press_args')) : nil
76
+
77
+ cmd = h.delete('command')
78
+ command = cmd ? Command.new(cmd, h.delete('args')) : nil
79
+
80
+ h.empty? or raise Error, 'unexpected JSON keys: ' << h.inspect
81
+ new(click, count, press_command, command)
82
+
83
+ rescue => ex
84
+ warn "error with binding #{json_hash.inspect}"
85
+ warn ex.message
86
+ raise
87
+ end
88
+
89
+ attr_reader :click, :count, :press_command, :command
90
+ attr_accessor :source_file
91
+
92
+ def initialize(click, count, press_command, command)
93
+ @click = click
94
+ @count = count
95
+ @press_command = press_command
96
+ @command = command
97
+ end
98
+
99
+ def to_dsl
100
+ spec = click.to_spec
101
+ dsl = "click#{count} #{spec.to_source}"
102
+ dsl << ", down: #{press_command.to_dsl}" if press_command
103
+ dsl << ", up: #{command.to_dsl}" if command
104
+
105
+ dsl
106
+ end
107
+
108
+ def to_h
109
+ h = click.to_h
110
+ h['count'] = count if count
111
+ h.merge! press_command.to_h('press_command', 'press_args') if press_command
112
+ h.merge! command.to_h if command
113
+ h
114
+ end
115
+
116
+ end
117
+
118
+
119
+ class DSLReader < Tools::BlankSlate
120
+
121
+ def initialize(file = nil)
122
+ @mousemaps = []
123
+ @current_map = nil
124
+ @mouse = Mouse.sublime
125
+ instance_eval File.read(file, encoding: 'utf-8'), file if file
126
+ end
127
+
128
+ def _mousemaps
129
+ @mousemaps
130
+ end
131
+
132
+ def mousemap(name, &block)
133
+ @current_map and raise Error, "'mousemap' blocks cannot be nested"
134
+ @current_map = MouseMap.new(name)
135
+ instance_eval(&block)
136
+ @mousemaps << @current_map
137
+ @current_map = nil
138
+ end
139
+
140
+ def button_names(maps={})
141
+ # create a copy of the Sublime mouse
142
+ @mouse = Mouse.sublime.clone('Custom Names')
143
+ maps.each_pair do |name, st_name|
144
+ b = @mouse.modifier(st_name) or
145
+ raise Error, "no button nor modifier named '#{st_name}'"
146
+ b.name = name
147
+ end
148
+ end
149
+
150
+ def click1(spec, options={})
151
+ click spec, options.merge({ count: 1 })
152
+ end
153
+
154
+ def click2(spec, options={})
155
+ click spec, options.merge({ count: 2 })
156
+ end
157
+
158
+ def click3(spec, options={})
159
+ click spec, options.merge({ count: 3 })
160
+ end
161
+
162
+ def click(spec, options={})
163
+ @current_map or raise Error, "'click' is invalid outside of a 'mousemap' block"
164
+ click = @mouse.ensure_click(spec)
165
+ count = options.delete(:count)
166
+ press_cmd = options.delete(:down)
167
+ press_cmd && press_cmd.error and raise Error, "click '#{spec}':#{press_cmd.error}"
168
+ cmd = options.delete(:up)
169
+ cmd && cmd.error and raise Error, "click '#{spec}':#{cmd.error}"
170
+ press_cmd || cmd or
171
+ raise Error, "click '#{spec}': no 'up' nor 'down' command"
172
+ @current_map.bindings << MouseBinding.new(click, count, press_cmd, cmd)
173
+ end
174
+
175
+ def method_missing(sym, *args, &block)
176
+ @current_map or raise Error, "'#{sym}' is invalid outside of a 'mousemap' block"
177
+ Command.from_method_missing(sym, args)
178
+ end
179
+
180
+ end
181
+
182
+ end
183
+
184
+ end
185
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+ class Package
6
+
7
+ ##
8
+ # 'Universal' DSL reader: reads any DSL block and updates its package.
9
+
10
+ class DSLReader
11
+
12
+ def initialize(file, package)
13
+ @file = file
14
+ @package = package
15
+ instance_eval File.read(file, encoding: 'utf-8'), file
16
+ end
17
+
18
+ def method_missing(sym, *args, &block)
19
+ raise DSLError, "'#{sym}' is not a valid Package DSL statement"
20
+ end
21
+
22
+ def theme(*args, &block)
23
+ r = TextMate::Theme::DSLReader.new
24
+ r.theme(*args, &block)
25
+ @package.themes.concat r._themes
26
+ end
27
+
28
+ def language(*args, &block)
29
+ r = TextMate::Grammar::DSLReader.new
30
+ r.language(*args, &block)
31
+ @package.grammars.concat r._grammars
32
+ end
33
+
34
+ def preferences(*args, &block)
35
+ r = TextMate::Preference::DSLReader.new
36
+ r.preferences(*args, &block)
37
+ @package.preferences.concat r._preferences
38
+ end
39
+
40
+ def snippets(*args, &block)
41
+ r = TextMate::Snippet::DSLReader.new
42
+ r.snippets(*args, &block)
43
+ @package.snippets.concat r._snippets
44
+ end
45
+
46
+ def settings(*args, &block)
47
+ r = SettingSet::DSLReader.new
48
+ r.settings(*args, &block)
49
+ @package.setting_sets.concat r._setting_sets
50
+ end
51
+
52
+ def macro(*args, &block)
53
+ r = Macro::DSLReader.new
54
+ r.macro(*args, &block)
55
+ @package.macros.concat r._macros
56
+ end
57
+
58
+ def commands(*args, &block)
59
+ r = CommandSet::DSLReader.new
60
+ r.commands(*args, &block)
61
+ @package.command_sets.concat r._command_sets
62
+ end
63
+
64
+ def menu(*args, &block)
65
+ r = Menu::DSLReader.new
66
+ r.menu(*args, &block)
67
+ @package.menus.concat r._menus
68
+ end
69
+
70
+ def mousemap(*args, &block)
71
+ r = MouseMap::DSLReader.new
72
+ r.mousemap(*args, &block)
73
+ @package.mousemaps.concat r._mousemaps
74
+ end
75
+
76
+ def keymap(*args, &block)
77
+ r = KeyMap::DSLReader.new
78
+ r._file = @file
79
+ r.keymap(*args, &block)
80
+ @package.keymaps.concat r._keymaps
81
+ end
82
+
83
+ def keyboard(*args, &block)
84
+ raise Error, "keyboards must be defined in a separate *.keyboard.rb file"
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+ end
91
+ end