sublime_dsl 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +136 -0
- data/Rakefile +248 -0
- data/SYNTAX.md +927 -0
- data/bin/subdsl +4 -0
- data/lib/sublime_dsl/cli/export.rb +134 -0
- data/lib/sublime_dsl/cli/import.rb +143 -0
- data/lib/sublime_dsl/cli.rb +125 -0
- data/lib/sublime_dsl/core_ext/enumerable.rb +24 -0
- data/lib/sublime_dsl/core_ext/string.rb +129 -0
- data/lib/sublime_dsl/core_ext.rb +4 -0
- data/lib/sublime_dsl/sublime_text/command.rb +157 -0
- data/lib/sublime_dsl/sublime_text/command_set.rb +112 -0
- data/lib/sublime_dsl/sublime_text/keyboard.rb +659 -0
- data/lib/sublime_dsl/sublime_text/keymap/dsl_reader.rb +194 -0
- data/lib/sublime_dsl/sublime_text/keymap.rb +385 -0
- data/lib/sublime_dsl/sublime_text/macro.rb +91 -0
- data/lib/sublime_dsl/sublime_text/menu.rb +237 -0
- data/lib/sublime_dsl/sublime_text/mouse.rb +149 -0
- data/lib/sublime_dsl/sublime_text/mousemap.rb +185 -0
- data/lib/sublime_dsl/sublime_text/package/dsl_reader.rb +91 -0
- data/lib/sublime_dsl/sublime_text/package/exporter.rb +138 -0
- data/lib/sublime_dsl/sublime_text/package/importer.rb +127 -0
- data/lib/sublime_dsl/sublime_text/package/reader.rb +102 -0
- data/lib/sublime_dsl/sublime_text/package/writer.rb +112 -0
- data/lib/sublime_dsl/sublime_text/package.rb +96 -0
- data/lib/sublime_dsl/sublime_text/setting_set.rb +123 -0
- data/lib/sublime_dsl/sublime_text.rb +48 -0
- data/lib/sublime_dsl/textmate/custom_base_name.rb +45 -0
- data/lib/sublime_dsl/textmate/grammar/dsl_reader.rb +383 -0
- data/lib/sublime_dsl/textmate/grammar/dsl_writer.rb +178 -0
- data/lib/sublime_dsl/textmate/grammar/plist_reader.rb +163 -0
- data/lib/sublime_dsl/textmate/grammar/plist_writer.rb +153 -0
- data/lib/sublime_dsl/textmate/grammar.rb +252 -0
- data/lib/sublime_dsl/textmate/plist.rb +141 -0
- data/lib/sublime_dsl/textmate/preference.rb +301 -0
- data/lib/sublime_dsl/textmate/snippet.rb +437 -0
- data/lib/sublime_dsl/textmate/theme/dsl_reader.rb +87 -0
- data/lib/sublime_dsl/textmate/theme/item.rb +74 -0
- data/lib/sublime_dsl/textmate/theme/plist_writer.rb +53 -0
- data/lib/sublime_dsl/textmate/theme.rb +364 -0
- data/lib/sublime_dsl/textmate.rb +9 -0
- data/lib/sublime_dsl/tools/blank_slate.rb +49 -0
- data/lib/sublime_dsl/tools/console.rb +74 -0
- data/lib/sublime_dsl/tools/helpers.rb +152 -0
- data/lib/sublime_dsl/tools/regexp_wannabe.rb +154 -0
- data/lib/sublime_dsl/tools/stable_inspect.rb +20 -0
- data/lib/sublime_dsl/tools/value_equality.rb +37 -0
- data/lib/sublime_dsl/tools/xml.rb +66 -0
- data/lib/sublime_dsl/tools.rb +66 -0
- data/lib/sublime_dsl.rb +23 -0
- 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
|