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