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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/lib/textmate_grammar/generated/grammar.rb +32 -0
  4. data/lib/textmate_grammar/generated/rule.rb +144 -0
  5. data/lib/textmate_grammar/grammar.rb +670 -0
  6. data/lib/textmate_grammar/grammar_plugin.rb +189 -0
  7. data/lib/textmate_grammar/import_patterns.rb +14 -0
  8. data/lib/textmate_grammar/linters/flat_includes.rb +32 -0
  9. data/lib/textmate_grammar/linters/includes_then_tag_as.rb +48 -0
  10. data/lib/textmate_grammar/linters/standard_naming.rb +226 -0
  11. data/lib/textmate_grammar/linters/start_match_empty.rb +49 -0
  12. data/lib/textmate_grammar/linters/tests.rb +19 -0
  13. data/lib/textmate_grammar/linters/unused_unresolved.rb +9 -0
  14. data/lib/textmate_grammar/pattern_extensions/look_ahead_for.rb +32 -0
  15. data/lib/textmate_grammar/pattern_extensions/look_ahead_to_avoid.rb +31 -0
  16. data/lib/textmate_grammar/pattern_extensions/look_behind_for.rb +31 -0
  17. data/lib/textmate_grammar/pattern_extensions/look_behind_to_avoid.rb +31 -0
  18. data/lib/textmate_grammar/pattern_extensions/lookaround_pattern.rb +169 -0
  19. data/lib/textmate_grammar/pattern_extensions/match_result_of.rb +67 -0
  20. data/lib/textmate_grammar/pattern_extensions/maybe.rb +50 -0
  21. data/lib/textmate_grammar/pattern_extensions/one_of.rb +107 -0
  22. data/lib/textmate_grammar/pattern_extensions/one_or_more_of.rb +42 -0
  23. data/lib/textmate_grammar/pattern_extensions/or_pattern.rb +55 -0
  24. data/lib/textmate_grammar/pattern_extensions/placeholder.rb +102 -0
  25. data/lib/textmate_grammar/pattern_extensions/recursively_match.rb +76 -0
  26. data/lib/textmate_grammar/pattern_extensions/zero_or_more_of.rb +50 -0
  27. data/lib/textmate_grammar/pattern_variations/base_pattern.rb +870 -0
  28. data/lib/textmate_grammar/pattern_variations/legacy_pattern.rb +61 -0
  29. data/lib/textmate_grammar/pattern_variations/pattern.rb +9 -0
  30. data/lib/textmate_grammar/pattern_variations/pattern_range.rb +233 -0
  31. data/lib/textmate_grammar/pattern_variations/repeatable_pattern.rb +204 -0
  32. data/lib/textmate_grammar/regex_operator.rb +182 -0
  33. data/lib/textmate_grammar/regex_operators/alternation.rb +24 -0
  34. data/lib/textmate_grammar/regex_operators/concat.rb +23 -0
  35. data/lib/textmate_grammar/stdlib/common.rb +20 -0
  36. data/lib/textmate_grammar/tokens.rb +110 -0
  37. data/lib/textmate_grammar/transforms/add_ending.rb +25 -0
  38. data/lib/textmate_grammar/transforms/bailout.rb +92 -0
  39. data/lib/textmate_grammar/transforms/fix_repeated_tag_as.rb +75 -0
  40. data/lib/textmate_grammar/transforms/resolve_placeholders.rb +121 -0
  41. data/lib/textmate_grammar/util.rb +198 -0
  42. data/lib/textmate_grammar.rb +4 -0
  43. 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'