ruby_grammar_builder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: 77eac15400d49048d663da889678d273a8904021ca833eef9deb989c6d9e613d
4
+ data.tar.gz: 6df5e9ebe420697d33c8d56c3c4fcb05087b67638718acac4923728ef80540b4
5
+ SHA512:
6
+ metadata.gz: aef9f1110484be3a2fd2d6e0c411a7265e7cdcb82dc9d4ff77cb098dd4c37ecbd5ea40c2d463ada5e9157b176237c8cc8cd50263d6f4360ea10d329b4ddd487b
7
+ data.tar.gz: 415d12672f81191a1e248bfee65632c146707eac9d16e82baa172e511775821aab21c11eca2ad631f5b84e033bb0757e2bbed8923569979373646f38b1ffcf61
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'