textmate_grammar 0.0.0
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/LICENSE +21 -0
- data/lib/textmate_grammar/generated/grammar.rb +32 -0
- data/lib/textmate_grammar/generated/rule.rb +144 -0
- data/lib/textmate_grammar/grammar.rb +670 -0
- data/lib/textmate_grammar/grammar_plugin.rb +189 -0
- data/lib/textmate_grammar/import_patterns.rb +14 -0
- data/lib/textmate_grammar/linters/flat_includes.rb +32 -0
- data/lib/textmate_grammar/linters/includes_then_tag_as.rb +48 -0
- data/lib/textmate_grammar/linters/standard_naming.rb +226 -0
- data/lib/textmate_grammar/linters/start_match_empty.rb +49 -0
- data/lib/textmate_grammar/linters/tests.rb +19 -0
- data/lib/textmate_grammar/linters/unused_unresolved.rb +9 -0
- data/lib/textmate_grammar/pattern_extensions/look_ahead_for.rb +32 -0
- data/lib/textmate_grammar/pattern_extensions/look_ahead_to_avoid.rb +31 -0
- data/lib/textmate_grammar/pattern_extensions/look_behind_for.rb +31 -0
- data/lib/textmate_grammar/pattern_extensions/look_behind_to_avoid.rb +31 -0
- data/lib/textmate_grammar/pattern_extensions/lookaround_pattern.rb +169 -0
- data/lib/textmate_grammar/pattern_extensions/match_result_of.rb +67 -0
- data/lib/textmate_grammar/pattern_extensions/maybe.rb +50 -0
- data/lib/textmate_grammar/pattern_extensions/one_of.rb +107 -0
- data/lib/textmate_grammar/pattern_extensions/one_or_more_of.rb +42 -0
- data/lib/textmate_grammar/pattern_extensions/or_pattern.rb +55 -0
- data/lib/textmate_grammar/pattern_extensions/placeholder.rb +102 -0
- data/lib/textmate_grammar/pattern_extensions/recursively_match.rb +76 -0
- data/lib/textmate_grammar/pattern_extensions/zero_or_more_of.rb +50 -0
- data/lib/textmate_grammar/pattern_variations/base_pattern.rb +870 -0
- data/lib/textmate_grammar/pattern_variations/legacy_pattern.rb +61 -0
- data/lib/textmate_grammar/pattern_variations/pattern.rb +9 -0
- data/lib/textmate_grammar/pattern_variations/pattern_range.rb +233 -0
- data/lib/textmate_grammar/pattern_variations/repeatable_pattern.rb +204 -0
- data/lib/textmate_grammar/regex_operator.rb +182 -0
- data/lib/textmate_grammar/regex_operators/alternation.rb +24 -0
- data/lib/textmate_grammar/regex_operators/concat.rb +23 -0
- data/lib/textmate_grammar/stdlib/common.rb +20 -0
- data/lib/textmate_grammar/tokens.rb +110 -0
- data/lib/textmate_grammar/transforms/add_ending.rb +25 -0
- data/lib/textmate_grammar/transforms/bailout.rb +92 -0
- data/lib/textmate_grammar/transforms/fix_repeated_tag_as.rb +75 -0
- data/lib/textmate_grammar/transforms/resolve_placeholders.rb +121 -0
- data/lib/textmate_grammar/util.rb +198 -0
- data/lib/textmate_grammar.rb +4 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a657074ac96248a5c684940b1b55c940e337bbece7561eec14836d48feeb7fca
|
4
|
+
data.tar.gz: 8738c1c4d6fab7559adf6017c44faf3e3eb72404249a1489381bc9b58cd496df
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 59d38e621cb65c51cb4125f94965930a7d863cbe17b160c30efb7597d177ca033abbbc2dd7e914d5ac8acaba8f58a41a60dcc9c88fc618c6e52fd764160ef29b
|
7
|
+
data.tar.gz: 29d4edea6da4762ae0395e9fbc2f0441dbbe2c3bd1a1820da432bea5e4518786e30b97a7f971a4528b758131a95738ec6cd51a7aa22ffaf00a91894e0cf496c1
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Jeff Hykin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Generated
|
4
|
+
class Grammar
|
5
|
+
# @return [String] The name of the grammar
|
6
|
+
attr_accessor :name
|
7
|
+
# @return [String] The grammars scope
|
8
|
+
attr_accessor :scope_name
|
9
|
+
# @return [String] The version of the grammar
|
10
|
+
attr_accessor :version
|
11
|
+
# @return [String] information for contributers
|
12
|
+
attr_accessor :information
|
13
|
+
# @return [PatternRule] rules in initial scope
|
14
|
+
attr_accessor :patterns
|
15
|
+
# @return [Hash<String=>Rule>] the repository of rules
|
16
|
+
attr_accessor :repository
|
17
|
+
# @return [Hash] other properties
|
18
|
+
attr_accessor :other_properties
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
default = {
|
22
|
+
"name" => @name,
|
23
|
+
"scopeName" => @scope_name,
|
24
|
+
"version" => @version,
|
25
|
+
"information_for_contributors" => @information,
|
26
|
+
"repository" => @repository.transform_values(&:to_h),
|
27
|
+
}
|
28
|
+
|
29
|
+
other_properties.merge(default).merge(@patterns.to_h)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Generated
|
4
|
+
class Rule
|
5
|
+
# @return [String] The location of this rule
|
6
|
+
attr_accessor :location
|
7
|
+
|
8
|
+
def initialize(location)
|
9
|
+
@location = location
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# Represents a rule in the form of { include = '#rule_name'; }
|
15
|
+
#
|
16
|
+
class IncludeRule < Rule
|
17
|
+
# @return [String] The included Rule name
|
18
|
+
attr_accessor :rule
|
19
|
+
|
20
|
+
def initialize(location, rule)
|
21
|
+
super(location)
|
22
|
+
@rule = rule
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
26
|
+
{"include" => @rule}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Represents a rule in the form of { name = 'string'; }
|
32
|
+
#
|
33
|
+
class NameRule < Rule
|
34
|
+
# @return [String] The name of the rule
|
35
|
+
attr_accessor :name
|
36
|
+
|
37
|
+
def initialize(location, name)
|
38
|
+
super(location)
|
39
|
+
@name = name
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_h
|
43
|
+
{"name" => @name}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Represents a rule in the form of { patterns = (Rule...); }
|
49
|
+
#
|
50
|
+
class PatternRule < Rule
|
51
|
+
# @return [Array<Rule>] The list of rules
|
52
|
+
attr_accessor :rules
|
53
|
+
|
54
|
+
def initialize(location, rules)
|
55
|
+
super(location)
|
56
|
+
@rules = rules
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_h
|
60
|
+
{"patterns" => @rules.map(&:to_h)}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class MatchRule < Rule
|
65
|
+
# @return [String] The match pattern
|
66
|
+
attr_accessor :match
|
67
|
+
# @return [String,nil] The name for this rule
|
68
|
+
attr_accessor :name
|
69
|
+
# @return [Hash<String=>Rule>] The capture rules
|
70
|
+
attr_accessor :captures
|
71
|
+
|
72
|
+
def initialize(location)
|
73
|
+
super(location)
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_h
|
77
|
+
{
|
78
|
+
"match" => @match,
|
79
|
+
"name" => @name,
|
80
|
+
"captures" => @captures.transform_values(&:to_h),
|
81
|
+
}.compact
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class BeginEndRule < Rule
|
86
|
+
# @return [String] The begin pattern
|
87
|
+
attr_accessor :begin
|
88
|
+
# @return [String] The end pattern
|
89
|
+
attr_accessor :end
|
90
|
+
# @return [String,nil] The name for this rule
|
91
|
+
attr_accessor :name
|
92
|
+
# @return [String,nil] The name for the contents matched
|
93
|
+
attr_accessor :contentName
|
94
|
+
# @return [Hash<String=>Rule>] The captures rules for begin
|
95
|
+
attr_accessor :beginCaptures
|
96
|
+
# @return [Hash<String=>Rule>] The captures rules for end
|
97
|
+
attr_accessor :endCaptures
|
98
|
+
|
99
|
+
def initialize(location)
|
100
|
+
super(location)
|
101
|
+
end
|
102
|
+
|
103
|
+
def to_h
|
104
|
+
{
|
105
|
+
"begin" => @begin,
|
106
|
+
"end" => @end,
|
107
|
+
"name" => @name,
|
108
|
+
"contentName" => @contentName,
|
109
|
+
"beginCaptures" => @beginCaptures.transform_values(&:to_h),
|
110
|
+
"endCaptures" => @endCaptures.transform_values(&:to_h),
|
111
|
+
}.compact
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class BeginWhileRule < Rule
|
116
|
+
# @return [String] The begin pattern
|
117
|
+
attr_accessor :begin
|
118
|
+
# @return [String] The while pattern
|
119
|
+
attr_accessor :while
|
120
|
+
# @return [String,nil] The name for this rule
|
121
|
+
attr_accessor :name
|
122
|
+
# @return [String,nil] The name for the contents matched
|
123
|
+
attr_accessor :contentName
|
124
|
+
# @return [Hash<String=>Rule>] The captures rules for begin
|
125
|
+
attr_accessor :beginCaptures
|
126
|
+
# @return [Hash<String=>Rule>] The captures rules for while
|
127
|
+
attr_accessor :whileCaptures
|
128
|
+
|
129
|
+
def initialize(location)
|
130
|
+
super(location)
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_h
|
134
|
+
{
|
135
|
+
"begin" => @begin,
|
136
|
+
"while" => @while,
|
137
|
+
"name" => @name,
|
138
|
+
"contentName" => @contentName,
|
139
|
+
"beginCaptures" => @beginCaptures.transform_values(&:to_h),
|
140
|
+
"whileCaptures" => @whileCaptures.transform_values(&:to_h),
|
141
|
+
}.compact
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,670 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'json'
|
5
|
+
require 'pp'
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
require_relative 'import_patterns'
|
9
|
+
|
10
|
+
#
|
11
|
+
# Represents a Textmate Grammar
|
12
|
+
#
|
13
|
+
class Grammar
|
14
|
+
#
|
15
|
+
# A mapping of grammar partials that have been exported
|
16
|
+
#
|
17
|
+
# @api private
|
18
|
+
#
|
19
|
+
@@export_grammars = {}
|
20
|
+
|
21
|
+
attr_accessor :repository
|
22
|
+
attr_accessor :name
|
23
|
+
attr_accessor :scope_name
|
24
|
+
|
25
|
+
#
|
26
|
+
# Create a new Exportable Grammar (Grammar Partial)
|
27
|
+
#
|
28
|
+
# @return [ExportableGrammar] the new exportable Grammar
|
29
|
+
#
|
30
|
+
def self.new_exportable_grammar
|
31
|
+
ExportableGrammar.new
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# import an existing grammar from a file
|
36
|
+
#
|
37
|
+
# @note the imported grammar is write only access to imported keys will raise an error
|
38
|
+
#
|
39
|
+
# @param [String] path path to a json or plist grammar
|
40
|
+
#
|
41
|
+
# @return [Grammar] The imported grammar
|
42
|
+
#
|
43
|
+
def self.fromTmLanguage(path)
|
44
|
+
begin
|
45
|
+
import_grammar = JSON.parse File.read(path)
|
46
|
+
rescue JSON::ParserError
|
47
|
+
require 'plist'
|
48
|
+
import_grammar = Plist.parse_xml File.read(path)
|
49
|
+
end
|
50
|
+
|
51
|
+
grammar = ImportGrammar.new(
|
52
|
+
name: import_grammar["name"],
|
53
|
+
scope_name: import_grammar["scopeName"],
|
54
|
+
version: import_grammar["version"] || "",
|
55
|
+
description: import_grammar["description"] || nil,
|
56
|
+
information_for_contributors: import_grammar["information_for_contributors"] || nil,
|
57
|
+
fileTypes: import_grammar["fileTypes"] || nil,
|
58
|
+
)
|
59
|
+
# import "patterns" into @repository[:$initial_context]
|
60
|
+
grammar.repository[:$initial_context] = import_grammar["patterns"]
|
61
|
+
# import the rest of the repository
|
62
|
+
import_grammar["repository"].each do |key, value|
|
63
|
+
# repository keys are kept as a hash
|
64
|
+
grammar.repository[key.to_sym] = value
|
65
|
+
end
|
66
|
+
grammar
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Import a grammar partial
|
71
|
+
#
|
72
|
+
# @note the import is "dynamic", changes made to the grammar partial after the import
|
73
|
+
# wil be reflected in the parent grammar
|
74
|
+
#
|
75
|
+
# @param [String, ExportableGrammar] path_or_export the grammar partial or the file
|
76
|
+
# in which the grammar partial is declared
|
77
|
+
#
|
78
|
+
# @return [ExportableGrammar]
|
79
|
+
#
|
80
|
+
def self.import(path_or_export)
|
81
|
+
export = path_or_export
|
82
|
+
unless path_or_export.is_a? ExportableGrammar
|
83
|
+
# allow for relative paths
|
84
|
+
if not Pathname.new(path_or_export).absolute?
|
85
|
+
relative_path = File.dirname(caller_locations[0].path)
|
86
|
+
if not Pathname.new(relative_path).absolute?
|
87
|
+
relative_path = File.join(Dir.pwd,relative_path)
|
88
|
+
end
|
89
|
+
path_or_export = File.join(relative_path, path_or_export)
|
90
|
+
end
|
91
|
+
require path_or_export
|
92
|
+
resolved = File.expand_path resolve_require(path_or_export)
|
93
|
+
|
94
|
+
export = @@export_grammars.dig(resolved, :grammar)
|
95
|
+
unless export.is_a? ExportableGrammar
|
96
|
+
raise "#{path_or_export} does not create a Exportable Grammar"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
return export.export
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Create a new Grammar
|
105
|
+
#
|
106
|
+
# @param [Hash] keys The grammar keys
|
107
|
+
# @option keys [String] :name The name of the grammar
|
108
|
+
# @option keys [String] :scope_name The scope_name of teh grammar, must start with
|
109
|
+
# +source.+ or +text.+
|
110
|
+
# @option keys [String, :auto] :version (:auto) the version of the grammar, :auto uses
|
111
|
+
# the current git commit as the version
|
112
|
+
# @option keys [Array] :patterns ([]) ignored, will be replaced with the initial context
|
113
|
+
# @option keys [Hash] :repository ({}) ignored, will be replaced by the generated rules
|
114
|
+
# @option keys all remaining options will be copied to the grammar without change
|
115
|
+
#
|
116
|
+
def initialize(keys)
|
117
|
+
required_keys = [:name, :scope_name]
|
118
|
+
unless required_keys & keys.keys == required_keys
|
119
|
+
puts "Missing one or more of the required grammar keys"
|
120
|
+
puts "Missing: #{required_keys - (required_keys & keys.keys)}"
|
121
|
+
puts "The required grammar keys are: #{required_keys}"
|
122
|
+
raise "See above error"
|
123
|
+
end
|
124
|
+
|
125
|
+
@name = keys[:name]
|
126
|
+
@scope_name = keys[:scope_name]
|
127
|
+
@repository = {}
|
128
|
+
|
129
|
+
keys.delete :name
|
130
|
+
keys.delete :scope_name
|
131
|
+
|
132
|
+
# auto versioning, when save_to is called grab the latest git commit or "" if not
|
133
|
+
# a git repo
|
134
|
+
keys[:version] ||= :auto
|
135
|
+
@keys = keys.compact
|
136
|
+
return if @scope_name == "export" || @scope_name.start_with?("source.", "text.")
|
137
|
+
|
138
|
+
puts "Warning: grammar scope name should start with `source.' or `text.'"
|
139
|
+
puts "Examples: source.cpp text.html text.html.markdown source.js.regexp"
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Access a pattern in the grammar
|
144
|
+
#
|
145
|
+
# @param [Symbol] key The key the pattern is stored in
|
146
|
+
#
|
147
|
+
# @return [PatternBase, Symbol, Array<PatternBase, Symbol>] The stored pattern
|
148
|
+
#
|
149
|
+
def [](key)
|
150
|
+
if key.is_a?(Regexp)
|
151
|
+
tokenMatching(key) # see tokens.rb
|
152
|
+
else
|
153
|
+
@repository.fetch(key, PlaceholderPattern.new(key))
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
#
|
158
|
+
# Store a pattern
|
159
|
+
#
|
160
|
+
# A pattern must be stored in the grammar for it to appear in the final grammar
|
161
|
+
#
|
162
|
+
# The special key :$initial_context is the pattern that will be matched at the
|
163
|
+
# beginning of the document or whenever the root of the grammar is to be matched
|
164
|
+
#
|
165
|
+
# @param [Symbol] key The key to store the pattern in
|
166
|
+
# @param [PatternBase, Symbol, Array<PatternBase, Symbol>] value the pattern to store
|
167
|
+
#
|
168
|
+
# @return [PatternBase, Symbol, Array<PatternBase, Symbol>] the stored pattern
|
169
|
+
#
|
170
|
+
def []=(key, value)
|
171
|
+
unless key.is_a? Symbol
|
172
|
+
raise "Use symbols not strings" unless key.is_a? Symbol
|
173
|
+
end
|
174
|
+
|
175
|
+
if key.to_s.start_with?("$") && !([:$initial_context, :$base, :$self].include? key)
|
176
|
+
puts "#{key} is not a valid repository name"
|
177
|
+
puts "repository names starting with $ are reserved"
|
178
|
+
raise "See above error"
|
179
|
+
end
|
180
|
+
|
181
|
+
if key.to_s == "repository"
|
182
|
+
puts "#{key} is not a valid repository name"
|
183
|
+
puts "the name 'repository' is a reserved name"
|
184
|
+
raise "See above error"
|
185
|
+
end
|
186
|
+
|
187
|
+
# add it to the repository
|
188
|
+
@repository[key] = fixup_value(value)
|
189
|
+
@repository[key]
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
# Import a grammar partial into this grammar
|
194
|
+
#
|
195
|
+
# @note the import is "dynamic", changes made to the grammar partial after the import
|
196
|
+
# wil be reflected in the parent grammar
|
197
|
+
#
|
198
|
+
# @param [String, ExportableGrammar] path_or_export the grammar partial or the file
|
199
|
+
# in which the grammar partial is declared
|
200
|
+
#
|
201
|
+
# @return [void] nothing
|
202
|
+
#
|
203
|
+
def import(path_or_export)
|
204
|
+
|
205
|
+
unless path_or_export.is_a? ExportableGrammar
|
206
|
+
relative_path = File.dirname(caller_locations[0].path)
|
207
|
+
if not Pathname.new(relative_path).absolute?
|
208
|
+
relative_path = File.join(Dir.pwd,relative_path)
|
209
|
+
end
|
210
|
+
# allow for relative paths
|
211
|
+
if not Pathname.new(path_or_export).absolute?
|
212
|
+
path_or_export = File.join(relative_path, path_or_export)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
export = Grammar.import(path_or_export)
|
217
|
+
export.parent_grammar = self
|
218
|
+
|
219
|
+
# import the repository
|
220
|
+
@repository = @repository.merge export.repository do |_key, old_val, new_val|
|
221
|
+
[old_val, new_val].flatten.uniq
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
#
|
226
|
+
# Runs a set of pre transformations
|
227
|
+
#
|
228
|
+
# @api private
|
229
|
+
#
|
230
|
+
# @param [Hash] repository The repository
|
231
|
+
# @param [:before_pre_linter,:after_pre_linter] stage the stage to run
|
232
|
+
#
|
233
|
+
# @return [Hash] the modified repository
|
234
|
+
#
|
235
|
+
def run_pre_transform_stage(repository, stage)
|
236
|
+
@@transforms[stage]
|
237
|
+
.sort { |a, b| a[:priority] <=> b[:priority] }
|
238
|
+
.map { |a| a[:transform] }
|
239
|
+
.each do |transform|
|
240
|
+
repository = repository.transform_values do |potential_pattern|
|
241
|
+
if potential_pattern.is_a? Array
|
242
|
+
potential_pattern.map do |each|
|
243
|
+
transform.pre_transform(
|
244
|
+
each,
|
245
|
+
filter_options(
|
246
|
+
transform,
|
247
|
+
each,
|
248
|
+
grammar: self,
|
249
|
+
repository: repository,
|
250
|
+
),
|
251
|
+
)
|
252
|
+
end
|
253
|
+
else
|
254
|
+
transform.pre_transform(
|
255
|
+
potential_pattern,
|
256
|
+
filter_options(
|
257
|
+
transform,
|
258
|
+
potential_pattern,
|
259
|
+
grammar: self,
|
260
|
+
repository: repository,
|
261
|
+
),
|
262
|
+
)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
repository
|
268
|
+
end
|
269
|
+
|
270
|
+
#
|
271
|
+
# Runs a set of post transformations
|
272
|
+
#
|
273
|
+
# @param [Hash] output The generated grammar
|
274
|
+
# @param [Symbol] stage the stage to run
|
275
|
+
#
|
276
|
+
# @return [Hash] The modified grammar
|
277
|
+
#
|
278
|
+
def run_post_transform_stage(output, stage)
|
279
|
+
@@transforms[stage]
|
280
|
+
.sort { |a, b| a[:priority] <=> b[:priority] }
|
281
|
+
.map { |a| a[:transform] }
|
282
|
+
.each { |transform| output = transform.post_transform(output) }
|
283
|
+
|
284
|
+
output
|
285
|
+
end
|
286
|
+
|
287
|
+
#
|
288
|
+
# Convert the grammar into a hash suitable for exporting to a file
|
289
|
+
#
|
290
|
+
# @param [Symbol] inherit_or_embedded Is this grammar being inherited
|
291
|
+
# from, or will be embedded, this controls if :$initial_context is mapped to
|
292
|
+
# +"$base"+ or +"$self"+
|
293
|
+
#
|
294
|
+
# @return [Hash] the generated grammar
|
295
|
+
#
|
296
|
+
def generate(options)
|
297
|
+
default = {
|
298
|
+
inherit_or_embedded: :embedded,
|
299
|
+
should_lint: true,
|
300
|
+
}
|
301
|
+
options = default.merge(options)
|
302
|
+
|
303
|
+
repo = @repository.__deep_clone__
|
304
|
+
repo = run_pre_transform_stage(repo, :before_pre_linter)
|
305
|
+
|
306
|
+
if options[:should_lint]
|
307
|
+
@@linters.each do |linter|
|
308
|
+
repo.each do |_, potential_pattern|
|
309
|
+
[potential_pattern].flatten.each do |each_potential_pattern|
|
310
|
+
raise "linting failed, see above error" unless linter.pre_lint(
|
311
|
+
each_potential_pattern,
|
312
|
+
filter_options(
|
313
|
+
linter,
|
314
|
+
each_potential_pattern,
|
315
|
+
grammar: self,
|
316
|
+
repository: repo,
|
317
|
+
),
|
318
|
+
)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
repo = run_pre_transform_stage(repo, :after_pre_linter)
|
325
|
+
|
326
|
+
convert_initial_context = lambda do |potential_pattern|
|
327
|
+
if potential_pattern == :$initial_context
|
328
|
+
return (options[:inherit_or_embedded] == :embedded) ? :$self : :$base
|
329
|
+
end
|
330
|
+
|
331
|
+
if potential_pattern.is_a? Array
|
332
|
+
return potential_pattern.map do |nested_potential_pattern|
|
333
|
+
convert_initial_context.call(nested_potential_pattern)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
if potential_pattern.is_a? PatternBase
|
338
|
+
return potential_pattern.transform_includes do |each_nested_potential_pattern|
|
339
|
+
# transform includes will call this block again if each_* is a patternBase
|
340
|
+
if each_nested_potential_pattern.is_a? PatternBase
|
341
|
+
next each_nested_potential_pattern
|
342
|
+
end
|
343
|
+
|
344
|
+
convert_initial_context.call(each_nested_potential_pattern)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
return potential_pattern
|
349
|
+
end
|
350
|
+
repo = repo.transform_values do |each_potential_pattern|
|
351
|
+
convert_initial_context.call(each_potential_pattern)
|
352
|
+
end
|
353
|
+
|
354
|
+
output = {
|
355
|
+
name: @name,
|
356
|
+
scopeName: @scope_name,
|
357
|
+
}
|
358
|
+
|
359
|
+
to_tag = lambda do |potential_pattern|
|
360
|
+
case potential_pattern
|
361
|
+
when Array
|
362
|
+
return {
|
363
|
+
"patterns" => potential_pattern.map do |nested_potential_pattern|
|
364
|
+
to_tag.call(nested_potential_pattern)
|
365
|
+
end,
|
366
|
+
}
|
367
|
+
when Symbol then return {"include" => "#" + potential_pattern.to_s}
|
368
|
+
when Hash then return potential_pattern
|
369
|
+
when String then return {"include" => potential_pattern}
|
370
|
+
when PatternBase then return potential_pattern.to_tag
|
371
|
+
else raise "Unexpected value: #{potential_pattern.class}"
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
output[:repository] = repo.transform_values do |each_potential_pattern|
|
376
|
+
to_tag.call(each_potential_pattern)
|
377
|
+
end
|
378
|
+
# sort repos by key name
|
379
|
+
output[:repository] = Hash[output[:repository].sort_by { |key, _| key.to_s }]
|
380
|
+
|
381
|
+
output[:patterns] = output[:repository][:$initial_context]
|
382
|
+
output[:patterns] ||= []
|
383
|
+
output[:patterns] = output[:patterns]["patterns"] if output[:patterns].is_a? Hash
|
384
|
+
output[:repository].delete(:$initial_context)
|
385
|
+
|
386
|
+
output[:version] = auto_version
|
387
|
+
output.merge!(@keys) { |_key, old, _new| old }
|
388
|
+
|
389
|
+
output = run_post_transform_stage(output, :before_pre_linter)
|
390
|
+
output = run_post_transform_stage(output, :after_pre_linter)
|
391
|
+
output = run_post_transform_stage(output, :before_post_linter)
|
392
|
+
|
393
|
+
@@linters.each do |linter|
|
394
|
+
raise "linting failed, see above error" unless linter.post_lint(output)
|
395
|
+
end
|
396
|
+
|
397
|
+
output = run_post_transform_stage(output, :after_post_linter)
|
398
|
+
|
399
|
+
Hash[
|
400
|
+
output.sort_by do |key, _|
|
401
|
+
order = {
|
402
|
+
information_for_contributors: 0,
|
403
|
+
version: 1,
|
404
|
+
name: 2,
|
405
|
+
scopeName: 3,
|
406
|
+
fileTypes: 4,
|
407
|
+
unknown_keys: 5,
|
408
|
+
patterns: 6,
|
409
|
+
repository: 7,
|
410
|
+
uuid: 8,
|
411
|
+
}
|
412
|
+
next order[key.to_sym] if order.has_key? key.to_sym
|
413
|
+
|
414
|
+
order[:unknown_keys]
|
415
|
+
end
|
416
|
+
]
|
417
|
+
end
|
418
|
+
|
419
|
+
#
|
420
|
+
# Save the grammar to a path
|
421
|
+
#
|
422
|
+
# @param [Hash] options options to save_to
|
423
|
+
# @option options :inherit_or_embedded (:embedded) see #generate
|
424
|
+
# @option options :generate_tags [Boolean] (true) generate a list of all +:tag_as+s
|
425
|
+
# @option options :directory [String] the location to generate the files
|
426
|
+
# @option options :tag_dir [String] (File.join(options[:directory],"language_tags")) the
|
427
|
+
# directory to generate language tags in
|
428
|
+
# @option options :syntax_dir [String] (File.join(options[:directory],"syntaxes")) the
|
429
|
+
# directory to generate the syntax file in
|
430
|
+
# @option options :syntax_format [:json,:vscode,:plist,:textmate,:tm_language,:xml]
|
431
|
+
# (:json) The format to generate the syntax file in
|
432
|
+
# @option options :syntax_name [String] ("#{@name}.tmLanguage") the name of the syntax
|
433
|
+
# file to generate without the extension
|
434
|
+
# @option options :tag_name [String] ("#{@name}-scopes.txt") the name of the tag list
|
435
|
+
# file to generate without the extension
|
436
|
+
#
|
437
|
+
# @note all keys except :directory is optional
|
438
|
+
# @note :directory is optional if both :tag_dir and :syntax_dir are specified
|
439
|
+
# @note currently :vscode is an alias for :json
|
440
|
+
# @note currently :textmate, :tm_language, and :xml are aliases for :plist
|
441
|
+
# @note later the aliased :syntax_type choices may enable compatibility features
|
442
|
+
#
|
443
|
+
# @return [void] nothing
|
444
|
+
#
|
445
|
+
def save_to(options)
|
446
|
+
options[:directory] ||= "."
|
447
|
+
|
448
|
+
# make the path absolute
|
449
|
+
absolute_path_from_caller = File.dirname(caller_locations[0].path)
|
450
|
+
if not Pathname.new(absolute_path_from_caller).absolute?
|
451
|
+
absolute_path_from_caller = File.join(Dir.pwd,absolute_path_from_caller)
|
452
|
+
end
|
453
|
+
if not Pathname.new(options[:directory]).absolute?
|
454
|
+
options[:directory] = File.join(absolute_path_from_caller, options[:directory])
|
455
|
+
end
|
456
|
+
|
457
|
+
default = {
|
458
|
+
inherit_or_embedded: :embedded,
|
459
|
+
generate_tags: true,
|
460
|
+
syntax_format: :json,
|
461
|
+
syntax_name: "#{@scope_name.split('.').drop(1).join('.')}",
|
462
|
+
syntax_dir: options[:directory],
|
463
|
+
tag_name: "#{@scope_name.split('.').drop(1).join('.')}_scopes.txt",
|
464
|
+
tag_dir: options[:directory],
|
465
|
+
should_lint: true,
|
466
|
+
}
|
467
|
+
options = default.merge(options)
|
468
|
+
|
469
|
+
output = generate(options)
|
470
|
+
|
471
|
+
if [:json, :vscode].include? options[:syntax_format]
|
472
|
+
file_name = File.join(
|
473
|
+
options[:syntax_dir],
|
474
|
+
"#{options[:syntax_name]}.tmLanguage.json",
|
475
|
+
)
|
476
|
+
out_file = File.open(file_name, "w")
|
477
|
+
out_file.write(JSON.pretty_generate(output))
|
478
|
+
out_file.close
|
479
|
+
elsif [:plist, :textmate, :tm_language, :xml].include? options[:syntax_format]
|
480
|
+
require 'plist'
|
481
|
+
file_name = File.join(
|
482
|
+
options[:syntax_dir],
|
483
|
+
options[:syntax_name],
|
484
|
+
)
|
485
|
+
out_file = File.open(file_name, "w")
|
486
|
+
out_file.write(Plist::Emit.dump(output))
|
487
|
+
out_file.close
|
488
|
+
else
|
489
|
+
puts "unexpected syntax format #{options[:syntax_format]}"
|
490
|
+
puts "expected one of [:json, :vscode, :plist, :textmate, :tm_language, :xml]"
|
491
|
+
raise "see above error"
|
492
|
+
end
|
493
|
+
|
494
|
+
return unless options[:generate_tags]
|
495
|
+
|
496
|
+
file_name = File.join(
|
497
|
+
options[:tag_dir],
|
498
|
+
options[:tag_name],
|
499
|
+
)
|
500
|
+
new_file = File.open(file_name, "w")
|
501
|
+
new_file.write(get_tags(output).to_a.sort.join("\n"))
|
502
|
+
new_file.close
|
503
|
+
end
|
504
|
+
|
505
|
+
#
|
506
|
+
# Returns the version information
|
507
|
+
#
|
508
|
+
# @api private
|
509
|
+
#
|
510
|
+
# @return [String] The version string to use
|
511
|
+
#
|
512
|
+
def auto_version
|
513
|
+
return @keys[:version] unless @keys[:version] == :auto
|
514
|
+
|
515
|
+
`git rev-parse HEAD`.strip
|
516
|
+
rescue StandardError
|
517
|
+
""
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
#
|
522
|
+
# Represents a partial Grammar object
|
523
|
+
#
|
524
|
+
# @note this has additional behavior to allow for partial grammars to reuse non exported keys
|
525
|
+
# @note only one may exist per file
|
526
|
+
#
|
527
|
+
class ExportableGrammar < Grammar
|
528
|
+
# @return [Array<Symbol>] names that will be exported by the grammar partial
|
529
|
+
attr_accessor :exports
|
530
|
+
# @return [Array<Symbol>] external names the this grammar partial may reference
|
531
|
+
attr_accessor :external_repos
|
532
|
+
#
|
533
|
+
# Grammars that are a parent to this grammar partial
|
534
|
+
#
|
535
|
+
# @api private
|
536
|
+
# @return [Grammar]
|
537
|
+
#
|
538
|
+
attr_accessor :parent_grammar
|
539
|
+
|
540
|
+
#
|
541
|
+
# Initialize a new Exportable Grammar
|
542
|
+
# @note use {Grammar.new_exportable_grammar} instead
|
543
|
+
#
|
544
|
+
def initialize
|
545
|
+
# skip: initialize, new, and new_exportable_grammar
|
546
|
+
location = caller_locations(3, 1).first
|
547
|
+
# and the first 5 bytes of the hash to get the seed
|
548
|
+
# will not be unique if multiple exportable grammars are created in the same file
|
549
|
+
# Don't do that
|
550
|
+
@seed = Digest::MD5.hexdigest(File.basename(location.path))[0..10]
|
551
|
+
super(
|
552
|
+
name: "export",
|
553
|
+
scope_name: "export"
|
554
|
+
)
|
555
|
+
|
556
|
+
if @@export_grammars[location.path].is_a? Hash
|
557
|
+
return if @@export_grammars[location.path][:line] == location.lineno
|
558
|
+
|
559
|
+
raise "Only one export grammar is allowed per file"
|
560
|
+
end
|
561
|
+
@@export_grammars[location.path] = {
|
562
|
+
line: location.lineno,
|
563
|
+
grammar: self,
|
564
|
+
}
|
565
|
+
|
566
|
+
@parent_grammar = []
|
567
|
+
end
|
568
|
+
|
569
|
+
#
|
570
|
+
# (see Grammar#[]=)
|
571
|
+
#
|
572
|
+
# @note grammar partials cannot constribute to $initial_context
|
573
|
+
#
|
574
|
+
def []=(key, value)
|
575
|
+
if key.to_s == "$initial_context"
|
576
|
+
puts "ExportGrammar cannot store to $initial_context"
|
577
|
+
raise "See error above"
|
578
|
+
end
|
579
|
+
super(key, value)
|
580
|
+
|
581
|
+
parent_grammar.each { |g| g.import self }
|
582
|
+
end
|
583
|
+
|
584
|
+
#
|
585
|
+
# Modifies the ExportableGrammar to namespace unexported keys
|
586
|
+
#
|
587
|
+
# @return [self]
|
588
|
+
#
|
589
|
+
def export
|
590
|
+
@repository.transform_keys! do |key|
|
591
|
+
next if [:$initial_context, :$base, :$self].include? key
|
592
|
+
|
593
|
+
# convert all repository keys to a prefixed version unless in exports
|
594
|
+
if key.to_s.start_with? @seed
|
595
|
+
# if exports has changed remove the seed
|
596
|
+
bare_key = (key.to_s.split(@seed + "_")[1]).to_sym
|
597
|
+
next bare_key if @exports.include? bare_key
|
598
|
+
|
599
|
+
next key
|
600
|
+
end
|
601
|
+
|
602
|
+
next key if @exports.include? key
|
603
|
+
|
604
|
+
(@seed + "_" + key.to_s).to_sym
|
605
|
+
end
|
606
|
+
# prefix all include symbols unless in external_repos or exports
|
607
|
+
@repository.transform_values! { |v| fixupValue(v) }
|
608
|
+
# ensure the grammar does not refer to a symbol not in repository or external_repos
|
609
|
+
# ensure the grammar has all keys named in exports
|
610
|
+
exports.each do |key|
|
611
|
+
unless @repository.has_key? key
|
612
|
+
raise "#{key} is exported but is missing in the repository"
|
613
|
+
end
|
614
|
+
end
|
615
|
+
self
|
616
|
+
end
|
617
|
+
|
618
|
+
private
|
619
|
+
|
620
|
+
def fixupValue(value)
|
621
|
+
# TDOD: rename this function it is too similar to fixup_value
|
622
|
+
if value.is_a? Symbol
|
623
|
+
return value if [:$initial_context, :$base, :$self].include? value
|
624
|
+
|
625
|
+
if value.to_s.start_with? @seed
|
626
|
+
# if exports or external_repos, has changed remove the seed
|
627
|
+
bare_value = (value.to_s.split(@seed + "_")[1]).to_sym
|
628
|
+
if @external_repos.include?(bare_value) || @exports.include?(bare_value)
|
629
|
+
return bare_value
|
630
|
+
end
|
631
|
+
|
632
|
+
return value
|
633
|
+
end
|
634
|
+
|
635
|
+
return value if @external_repos.include?(value) || @exports.include?(value)
|
636
|
+
|
637
|
+
(@seed + "_" + value.to_s).to_sym
|
638
|
+
elsif value.is_a? Array
|
639
|
+
value.map { |v| fixupValue(v) }
|
640
|
+
elsif value.is_a? PatternBase
|
641
|
+
value
|
642
|
+
else
|
643
|
+
raise "Unexpected object of type #{value.class} in value"
|
644
|
+
end
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
#
|
649
|
+
# Represents a Textmate Grammar that has been imported
|
650
|
+
# This exists entirely to override Grammar#[] and should not be
|
651
|
+
# normally created
|
652
|
+
#
|
653
|
+
# @api private
|
654
|
+
#
|
655
|
+
class ImportGrammar < Grammar
|
656
|
+
# (see Grammar#initialize)
|
657
|
+
def initialize(keys)
|
658
|
+
super(keys)
|
659
|
+
end
|
660
|
+
|
661
|
+
# (see Grammar#[])
|
662
|
+
# @note patterns that have been imported from a file cannot be be accessed
|
663
|
+
def [](key)
|
664
|
+
raise "#{key} is a not a pattern and cannot be referenced" if @repository[key].is_a? Hash
|
665
|
+
|
666
|
+
@repository[key]
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
require_relative 'grammar_plugin'
|