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,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