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,163 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module TextMate
5
+ class Grammar
6
+
7
+ ##
8
+ # Creates a Grammar from a PList.
9
+
10
+ class PListReader
11
+
12
+ attr_reader :grammar
13
+
14
+ def initialize(file)
15
+ @grammar = Grammar.new(nil, nil)
16
+ read_plist PList.import(file)
17
+ @grammar.basename = File.basename(file, File.extname(file))
18
+ @grammar.complete!
19
+ end
20
+
21
+ private
22
+
23
+ def read_plist(root)
24
+
25
+ grammar.name = root.delete('name')
26
+ grammar.scope = root.delete('scopeName')
27
+
28
+ root.each_pair do |key, value|
29
+ case key
30
+ when 'comment'
31
+ grammar.comment = cleanup(value)
32
+ when 'fileTypes'
33
+ grammar.file_types = value # an array of strings
34
+ when *%w(foldingStartMarker foldingStopMarker firstLineMatch)
35
+ grammar.send key.snake_case + '=', regexp(value)
36
+ when *%w(keyEquivalent bundleUUID uuid)
37
+ grammar.send key.snake_case + '=', value
38
+ when 'patterns'
39
+ grammar.patterns = patterns(value, grammar) # an array
40
+ when 'repository'
41
+ value.each_pair do |name, content|
42
+ f = Fragment.new(name)
43
+ f.patterns = scan(content, f)
44
+ f.patterns = [f.patterns] unless f.patterns.is_a? Array
45
+ grammar.fragments << f
46
+ end
47
+ when 'injections'
48
+ warn "grammar #{grammar}: #{key.inspect} is not supported"
49
+ else
50
+ warn "unknown key in grammar #{grammar}: #{key.inspect}"
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ def patterns(list, parent)
57
+ list.map { |v| scan v, parent }.flatten
58
+ end
59
+
60
+ def scan(value, parent)
61
+ value.is_a?(Hash) or raise Error, "expected a Hash: #{value.inspect}"
62
+ hash = value.dup
63
+ comment = cleanup(hash.delete('comment'))
64
+
65
+ if (inc = hash['include'])
66
+ hash.length == 1 or raise Error, "include: too many keys: #{value.inspect}"
67
+ inc = Include.new(inc)
68
+ inc.comment = comment
69
+ return inc
70
+ end
71
+
72
+ # do not create a rule that just contains patterns: return the patterns instead
73
+ if hash.length == 1 && hash['patterns']
74
+ if comment
75
+ parent.comment and raise Error, 'comment conflict'
76
+ parent.comment = comment
77
+ end
78
+ return patterns(hash['patterns'], parent)
79
+ end
80
+
81
+ if (match = hash.delete('match'))
82
+ rule = MatchRule.new
83
+ caps = hash.delete('captures')
84
+ if hash['beginCaptures']
85
+ caps and raise Error, "both 'beginCaptures' and 'captures': " << value.inspect
86
+ warn "grammar '#{grammar}': 'beginCaptures' understood as 'captures'" \
87
+ " in 'match' rule: " << value.inspect
88
+ caps = hash.delete('beginCaptures')
89
+ end
90
+ rule.match = create_match(match, caps)
91
+ elsif hash['begin'] || hash['end']
92
+ rule = BeginEndRule.new
93
+ rule.content_scope = hash.delete('contentName')
94
+ bcap = hash.delete('beginCaptures')
95
+ ecap = hash.delete('endCaptures')
96
+ caps = hash.delete('captures')
97
+ assign_captures rule, caps if caps
98
+ beg_re = hash.delete('begin')
99
+ unless beg_re
100
+ warn "no 'begin' for rule with 'end' in grammar #{grammar}"
101
+ beg_re = ''
102
+ end
103
+ end_re = hash.delete('end')
104
+ unless end_re
105
+ warn "no 'end' for rule with 'begin' in grammar #{grammar}"
106
+ end_re = ''
107
+ end
108
+ rule.from = create_match(beg_re, bcap)
109
+ rule.to = create_match(end_re, ecap, rule.from.regexp)
110
+ rule.to_last = hash.delete('applyEndPatternLast')
111
+ else
112
+ warn "grammar #{grammar}: no 'begin' nor 'match': " << hash.inspect
113
+ rule = NoMatchRule.new
114
+ end
115
+
116
+ rule.scope = hash.delete('name')
117
+ rule.comment = comment
118
+ rule.disabled = hash.delete('disabled')
119
+ rule.patterns = patterns(hash.delete('patterns'), rule) if hash['patterns']
120
+ hash.length == 0 or warn "invalid rule keys in grammar #{grammar}: #{hash.inspect}"
121
+
122
+ rule.complete!(grammar)
123
+
124
+ rule
125
+ end
126
+
127
+ def create_match(re, captures, backref = nil)
128
+ m = Match.new(regexp(re, backref))
129
+ assign_captures m, captures if captures
130
+ m
131
+ end
132
+
133
+ # Assign the PList +captures+ to +object+ (a Match or a BeginEndRule).
134
+ def assign_captures(object, captures)
135
+ # get the captures, making sure the keys are in ascending order
136
+ cap = []
137
+ captures.each_pair do |k,v|
138
+ name = v['name']
139
+ if name.nil?
140
+ warn "invalid capture in grammar #{grammar}: #{v.inspect}"
141
+ next
142
+ elsif v.length > 1
143
+ warn "extra capture ignored in grammar #{grammar}: #{v.inspect}"
144
+ end
145
+ cap[k.to_i] = name unless name.empty?
146
+ end
147
+ cap.each_with_index { |name, index| object.captures[index] = name if name }
148
+ end
149
+
150
+ # Replaces tabs by 2 spaces & dedents.
151
+ def cleanup(text)
152
+ text && text.gsub("\t", ' ').dedent
153
+ end
154
+
155
+ def regexp(str, backref = nil)
156
+ Tools::RegexpWannabe.new(str, backref)
157
+ end
158
+
159
+ end
160
+
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,153 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module TextMate
5
+ class Grammar
6
+
7
+ ##
8
+ # Creates the PList for a grammar.
9
+
10
+ class PListWriter
11
+
12
+ attr_reader :grammar
13
+ attr_reader :root
14
+
15
+ def initialize(grammar)
16
+ @grammar = grammar
17
+ @root = {}
18
+ convert_grammar
19
+ end
20
+
21
+ def export(file)
22
+ PList.export(root, file)
23
+ end
24
+
25
+ private
26
+
27
+ def convert_grammar
28
+
29
+ root['name'] = grammar.name
30
+ root['scopeName'] = grammar.scope
31
+
32
+ %w(fileTypes firstLineMatch foldingStartMarker foldingStopMarker keyEquivalent uuid bundleUUID).each do |att|
33
+ root[att] = convert_object(grammar.send(att.snake_case))
34
+ end
35
+
36
+ root['patterns'] = convert_array(grammar.patterns)
37
+
38
+ frags = {}
39
+ grammar.fragments.each { |f| frags[f.name] = convert_fragment(f) }
40
+ root['repository'] = frags
41
+
42
+ cleanup_hash root
43
+
44
+ end
45
+
46
+ def convert_object(object)
47
+ k = object.class.name.split('::').last.snake_case
48
+ send "convert_#{k}", object
49
+ end
50
+
51
+ def convert_array(list)
52
+ list.map { |o| convert_object o }
53
+ end
54
+
55
+ def convert_fragment(f)
56
+ unless f.used
57
+ warn "grammar '#{grammar.name}': fragment '#{f.name}' never used, not output"
58
+ return nil
59
+ end
60
+ a = convert_array(f.patterns)
61
+ a.length > 1 ? { 'patterns' => a } : a.first
62
+ end
63
+
64
+ def convert_include(inc)
65
+ { 'include' => inc.fragment_name }
66
+ end
67
+
68
+ def convert_no_match_rule(rule)
69
+ return nil if rule.empty?
70
+ h = convert_rule_start(rule)
71
+ h['patterns'] = convert_array(rule.patterns)
72
+ h
73
+ end
74
+
75
+ def convert_match_rule(rule)
76
+ h = convert_rule_start(rule)
77
+ h.merge! convert_match(rule.match, 'match', 'captures')
78
+ end
79
+
80
+ def convert_begin_end_rule(rule)
81
+ h = convert_rule_start(rule)
82
+ h['contentName'] = rule.content_scope
83
+ h.merge! convert_match(rule.from, 'begin', 'beginCaptures')
84
+ h.merge! convert_match(rule.to, 'end', 'endCaptures')
85
+ h.merge! convert_captures('captures', rule.captures)
86
+ h['applyEndPatternLast'] = convert_object(rule.to_last)
87
+ h['patterns'] = convert_array(rule.patterns)
88
+ h
89
+ end
90
+
91
+ def convert_rule_start(rule)
92
+ { 'name' => rule.scope, 'disabled' => convert_object(rule.disabled) }
93
+ end
94
+
95
+ def convert_match(match, regexp_key, captures_key)
96
+ { regexp_key => convert_regexp_wannabe(match.regexp) }
97
+ .merge convert_captures(captures_key, match.captures)
98
+ end
99
+
100
+ def convert_regexp_wannabe(re)
101
+ re.to_s
102
+ end
103
+
104
+ def convert_captures(captures_key, captures)
105
+ h = {}
106
+ return h if captures.empty?
107
+ captures.each_pair do |number, scope|
108
+ h[number.to_s] = { 'name' => scope }
109
+ end
110
+ { captures_key => h }
111
+ end
112
+
113
+ def convert_string(v); v; end;
114
+ def convert_nil_class(v); v; end
115
+ def convert_true_class(v); 1; end
116
+ def convert_false_class(v); 0; end
117
+
118
+ def cleanup_object(o)
119
+ case o
120
+ when Array; cleanup_array o
121
+ when Hash; cleanup_hash o
122
+ end
123
+ end
124
+
125
+ def cleanup_array(list)
126
+ list.each { |o| cleanup_object o }
127
+ list.reject! { |o| empty(o) }
128
+ end
129
+
130
+ def cleanup_hash(h)
131
+ h.keys.each do |key|
132
+ value = h[key]
133
+ cleanup_object value
134
+ if empty(value)
135
+ # HACK: an empty 'begin' or 'end' matches anything (used by C & Tcl grammars)
136
+ if key != 'end' && key != 'begin'
137
+ h.delete key
138
+ else
139
+ h[key] = ''
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ def empty(o)
146
+ o.nil? || o.respond_to?(:empty?) && o.empty?
147
+ end
148
+
149
+ end
150
+
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,252 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'grammar/plist_reader'
4
+ require_relative 'grammar/dsl_writer'
5
+ require_relative 'grammar/dsl_reader'
6
+ require_relative 'grammar/plist_writer'
7
+
8
+ module SublimeDSL
9
+ module TextMate
10
+
11
+ ##
12
+ # A language grammar.
13
+
14
+ class Grammar
15
+
16
+ # Create from a PList file.
17
+ def self.import(file)
18
+ PListReader.new(file).grammar
19
+ end
20
+
21
+ include CustomBaseName
22
+
23
+ attr_accessor :name
24
+ attr_accessor :scope
25
+ attr_accessor :file_types
26
+ attr_accessor :comment
27
+ attr_accessor :first_line_match
28
+ attr_accessor :folding_start_marker
29
+ attr_accessor :folding_stop_marker
30
+
31
+ # TextMate only
32
+ attr_accessor :key_equivalent
33
+ attr_accessor :uuid
34
+ attr_accessor :bundle_uuid
35
+
36
+ # content
37
+ attr_accessor :patterns
38
+ attr_accessor :fragments
39
+
40
+ def initialize(name, scope)
41
+
42
+ @name = name
43
+ @scope = scope
44
+
45
+ @file_types = nil
46
+ @first_line_match = nil
47
+ @folding_start = nil
48
+ @folding_stop = nil
49
+
50
+ @key_equivalent = nil
51
+ @uuid = nil
52
+
53
+ @patterns = []
54
+ @fragments = []
55
+
56
+ end
57
+
58
+ alias to_s name
59
+
60
+ def complete!
61
+ @repos = fragments.keyed_by(&:name)
62
+ resolve_includes self
63
+ fragments.each { |f| resolve_includes f }
64
+ end
65
+
66
+ def resolve_includes(parent)
67
+ parent.patterns.each do |o|
68
+ if o.respond_to? :patterns
69
+ resolve_includes o
70
+ elsif o.is_a?(Include) && o.fragment_name.start_with?('#')
71
+ name = o.fragment_name[1..-1]
72
+ o.fragment = @repos[name]
73
+ if o.fragment
74
+ o.fragment.used = true
75
+ else
76
+ warn "grammar #{self.name}: no such fragment: #{o.fragment_name.inspect}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def write(dir)
83
+ file = "#{dir}/#{basename}.tmLanguage.rb"
84
+ File.open(file, 'wb:utf-8') do |f|
85
+ f.puts '# encoding: utf-8'
86
+ f.puts "\n" << DSLWriter.new(self).dsl
87
+ end
88
+ end
89
+
90
+ def export(dir)
91
+ file = "#{dir}/#{basename}.tmLanguage"
92
+ PListWriter.new(self).export(file)
93
+ end
94
+
95
+ ##
96
+ # A repository item.
97
+
98
+ class Fragment
99
+ attr_reader :name
100
+ attr_accessor :comment
101
+ attr_accessor :patterns
102
+ attr_accessor :used
103
+ def initialize(name)
104
+ @name = name
105
+ @comment = nil
106
+ @patterns = []
107
+ @used = nil
108
+ end
109
+ end
110
+
111
+ ##
112
+ # An included fragment.
113
+
114
+ class Include
115
+ attr_reader :fragment_name
116
+ attr_accessor :fragment
117
+ attr_accessor :comment
118
+ def initialize(fragment_name)
119
+ @fragment_name = fragment_name
120
+ end
121
+ end
122
+
123
+ ##
124
+ # An abstract rule.
125
+
126
+ class Rule
127
+ attr_accessor :scope
128
+ attr_accessor :comment
129
+ attr_accessor :disabled
130
+ def complete!(grammar) end
131
+ end
132
+
133
+ ##
134
+ # A 'match' rule.
135
+
136
+ class MatchRule < Rule
137
+ attr_accessor :match # Match object
138
+ end
139
+
140
+ ##
141
+ # A 'begin/end' rule.
142
+
143
+ class BeginEndRule < Rule
144
+
145
+ attr_accessor :content_scope
146
+ attr_accessor :from # Match object
147
+ attr_accessor :to # Match object
148
+ attr_accessor :to_last
149
+ attr_reader :captures # common captures, a hash { number => scope }
150
+ attr_accessor :patterns
151
+
152
+ def initialize
153
+ @patterns = []
154
+ @captures = {}
155
+ end
156
+
157
+ def complete!(grammar)
158
+ captures.each_pair do |index, scope|
159
+ fscope = from.captures[index]
160
+ tscope = to.captures[index]
161
+ if fscope
162
+ if tscope == fscope
163
+ if fscope == scope
164
+ # from scope == to scope == common scope => just keep common
165
+ from.captures.delete(index)
166
+ to.captures.delete(index)
167
+ else
168
+ # from scope == to scope != common scope => set common = from/to
169
+ warn "grammar #{grammar}: 'both' capture #{index} => #{scope.inspect} replaced by 'from/to' capture #{index} => #{fscope.inspect}"
170
+ captures[index] = fscope
171
+ from.captures.delete index
172
+ to.captures.delete index
173
+ end
174
+ elsif tscope.nil?
175
+ if fscope == scope
176
+ # from scope == common scope, no 'to' scope => just keep common
177
+ from.captures.delete(index)
178
+ else
179
+ # from scope != common scope, no 'to' scope => common become 'to'
180
+ warn "grammar #{grammar}: 'both' capture #{index} => #{scope.inspect} moved to 'to' ('from' has #{index} => #{fscope.inspect})"
181
+ add_capture captures.delete(index), index, to.captures
182
+ end
183
+ else
184
+ # from scope != to scope => ignore common
185
+ warn "grammar #{grammar}: 'both' capture #{index} => #{scope.inspect} ignored: 'from' and 'to' already given"
186
+ captures.delete(index)
187
+ end
188
+ elsif tscope.nil?
189
+ # both fscope & tscope nil: ok
190
+ else
191
+ if tscope == scope
192
+ # to scope == common scope, no 'from' scope => just keep common
193
+ to.captures.delete(index)
194
+ else
195
+ # to scope != common scope, no 'from' scope => common become 'from'
196
+ warn "grammar #{grammar}: 'both' capture #{index} => #{scope.inspect} moved to 'from' ('to' has #{index} => #{tscope.inspect})"
197
+ add_capture captures.delete(index), index, from.captures
198
+ end
199
+ end
200
+ end
201
+ if !from.captures.empty? && from.captures == to.captures
202
+ captures.merge! from.captures
203
+ from.captures.clear
204
+ to.captures.clear
205
+ end
206
+ end
207
+
208
+ def add_capture(scope, index, captures)
209
+ captures[index] = scope
210
+ h = {}
211
+ captures.keys.sort.each do |i|
212
+ h[i] = captures[i]
213
+ end
214
+ captures.clear
215
+ captures.merge! h
216
+ end
217
+
218
+ end
219
+
220
+ ##
221
+ # A rule without 'match' nor 'begin'
222
+
223
+ class NoMatchRule < Rule
224
+
225
+ attr_accessor :patterns
226
+
227
+ def initialize
228
+ @patterns = []
229
+ end
230
+
231
+ def empty?
232
+ scope.nil? && patterns.empty?
233
+ end
234
+
235
+ end
236
+
237
+ ##
238
+ # A regexp with its captures.
239
+
240
+ class Match
241
+ attr_reader :regexp
242
+ attr_reader :captures # hash { number => scope }
243
+ def initialize(regexp)
244
+ @regexp = regexp
245
+ @captures = {}
246
+ end
247
+ end
248
+
249
+ end
250
+
251
+ end
252
+ end
@@ -0,0 +1,141 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module TextMate
5
+
6
+ ##
7
+ # Tools to read and write hashes in PList format.
8
+
9
+ module PList
10
+
11
+ class << self
12
+
13
+ # Returns a Hash read from +file+. See ::load.
14
+ def import(file)
15
+ File.open(file, 'r:utf-8') { |f| load(f) }
16
+ end
17
+
18
+ # Writes the Hash +hash+ to +file+ in PList format.
19
+ def export(hash, file)
20
+ File.open(file, 'wb:utf-8') { |f| f.write dump(hash) }
21
+ end
22
+
23
+ # Returns a Hash corresponding to the root +dict+ of the PList.
24
+ def load(string_or_io)
25
+ doc = Tools::XML.load(string_or_io)
26
+ # Document name="document" children = [
27
+ # DTD
28
+ # Element name="plist" children = [
29
+ # Element name="dict" children = [
30
+ # Element name="key" children = [ Text "author" ]
31
+ # Element name="string" children = [ Text "Z comme Zorglub" ]
32
+ # Element name = "key" children = [ Text "name" ]
33
+ # Element name = "string" children = [ Text "Zorgléoptère" ]
34
+ # ...
35
+ # ]
36
+ # ]
37
+ # ]
38
+ root_node = doc.children.last.children.first
39
+ load_node(root_node)
40
+ end
41
+
42
+ # Returns a String containing the PList XML for +hash+.
43
+ def dump(hash)
44
+ <<-XML.dedent << dump_hash(hash, '') << '</plist>'
45
+ <?xml version="1.0" encoding="UTF-8"?>
46
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
47
+ <plist version="1.0">
48
+ XML
49
+ end
50
+
51
+ private
52
+
53
+ def load_node(node)
54
+ if node.comment?
55
+ # warn 'plist: comment node ignored'
56
+ return nil
57
+ end
58
+ case node.name
59
+ when 'dict'
60
+ load_dict(node)
61
+ when 'string'
62
+ node.text
63
+ when 'integer'
64
+ node.text.to_i
65
+ when 'array'
66
+ node.children.map { |n| load_node n }.compact
67
+ when 'true'
68
+ true
69
+ when 'false'
70
+ false
71
+ else
72
+ raise Error, "unexpected: #{node.name.inspect}"
73
+ end
74
+ end
75
+
76
+ def load_dict(node)
77
+ h = {}
78
+ # comments, list = node.children.partition(&:comment?)
79
+ # warn 'plist: comment node(s) ignored in dict' unless comments.empty?
80
+ list = node.children.reject(&:comment?)
81
+ n = list.length / 2
82
+ (0...n).each do |i|
83
+ i = i * 2
84
+ k = list[i]
85
+ v = list[i+1]
86
+ k.name == 'key' or raise Error, "expected 'key': #{k.name.inspect}"
87
+ v = load_node(v)
88
+ v.nil? and raise Error, "comment as value for key '#{k.text}'"
89
+ h[k.text] = v
90
+ end
91
+ h
92
+ end
93
+
94
+ def dump_object(object, indent)
95
+ case object
96
+ when String; dump_string(object, indent)
97
+ when Fixnum; dump_fixnum(object, indent)
98
+ when Hash; dump_hash(object, indent)
99
+ when Array; dump_array(object, indent)
100
+ when TrueClass; "#{indent}<true/>\n"
101
+ when FalseClass; "#{indent}<false/>\n"
102
+ else; raise Error, "unexpected type: #{object.class.name}"
103
+ end
104
+ end
105
+
106
+ def dump_string(str, indent)
107
+ "#{indent}<string>" << h(str) << "</string>\n"
108
+ end
109
+
110
+ def dump_fixnum(num, indent)
111
+ "#{indent}<integer>#{num}</integer>\n"
112
+ end
113
+
114
+ def dump_hash(hash, indent)
115
+ return "#{indent}<dict/>\n" if hash.empty?
116
+ result = "#{indent}<dict>\n"
117
+ i = indent + "\t"
118
+ hash.keys.sort.each do |key|
119
+ result << "#{i}<key>#{key}</key>\n" << dump_object(hash[key], i)
120
+ end
121
+ result << "#{indent}</dict>\n"
122
+ end
123
+
124
+ def dump_array(array, indent)
125
+ return "#{indent}<array/>\n" if array.empty?
126
+ result = "#{indent}<array>\n"
127
+ i = indent + "\t"
128
+ array.each { |o| result << dump_object(o, i) }
129
+ result << "#{indent}</array>\n"
130
+ end
131
+
132
+ def h(text)
133
+ text.html_escape(false)
134
+ end
135
+
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+ end