antares 0.1.0 → 0.2.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.
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "tm_language_plist"
5
+ require_relative "tm_language_lexer"
6
+
7
+ module Antares
8
+ module Grammar
9
+ MAX_BYTES = 2 * 1024 * 1024
10
+ MAX_DEPTH = 64
11
+ MAX_RULES = 10_000
12
+ MAX_REGEX_BYTES = 16_384
13
+ TOP_KEYS = %w[$schema name scopeName fileTypes firstLineMatch foldingStartMarker
14
+ foldingStopMarker patterns repository uuid version comment
15
+ information_for_contributors hideFromUser].freeze
16
+ RULE_KEYS = %w[include match captures name begin end beginCaptures endCaptures
17
+ contentName patterns applyEndPatternLast comment].freeze
18
+ UNSUPPORTED_RULE_KEYS = %w[while whileCaptures repository disabled].freeze
19
+
20
+ MatchRule = Struct.new(:regexp, :token, :captures, keyword_init: true)
21
+ BeginRule = Struct.new(:regexp, :end_source, :ending, :dynamic_end, :token,
22
+ :content_token, :begin_captures, :end_captures, :patterns, :apply_end_last,
23
+ keyword_init: true)
24
+ IncludeRule = Struct.new(:target, keyword_init: true)
25
+ GroupRule = Struct.new(:patterns, keyword_init: true)
26
+ Definition = Struct.new(:scope_name, :tag, :patterns, :repository, keyword_init: true)
27
+
28
+ class UniqueHash < Hash
29
+ def []=(key, value)
30
+ raise GrammarError, "duplicate JSON object key #{key.inspect}" if key?(key)
31
+
32
+ super
33
+ end
34
+ end
35
+ private_constant :UniqueHash
36
+
37
+ module_function
38
+
39
+ def load_tmlanguage(path)
40
+ raise ArgumentError, "path must be a String" unless path.is_a?(String) && !path.include?("\0")
41
+
42
+ source = File.open(path, "rb") do |file|
43
+ raise GrammarError, "tmLanguage must be a regular file" unless file.stat.file?
44
+ raise ResourceLimitError, "tmLanguage exceeds #{MAX_BYTES} bytes" if file.stat.size > MAX_BYTES
45
+
46
+ file.read(MAX_BYTES + 1) || "".b
47
+ end
48
+ raise ResourceLimitError, "tmLanguage exceeds #{MAX_BYTES} bytes" if source.bytesize > MAX_BYTES
49
+ source = source.delete_prefix("\xEF\xBB\xBF".b).force_encoding(Encoding::UTF_8)
50
+ raise GrammarError, "tmLanguage must be valid UTF-8" unless source.valid_encoding?
51
+
52
+ value = parse(source)
53
+ definition = Compiler.new.compile(value)
54
+ TMLanguageLexer.build(definition)
55
+ rescue SystemCallError => error
56
+ raise GrammarError, "cannot read tmLanguage: #{error.message}"
57
+ end
58
+
59
+ def parse(source)
60
+ case source.lstrip.getbyte(0)
61
+ when 123
62
+ JSON.parse(source, max_nesting: MAX_DEPTH, object_class: UniqueHash,
63
+ allow_duplicate_key: false)
64
+ when 60
65
+ TMLanguagePlist.parse(source, max_depth: MAX_DEPTH)
66
+ else
67
+ raise GrammarError, "tmLanguage must be JSON or XML plist"
68
+ end
69
+ rescue JSON::ParserError => error
70
+ raise GrammarError, "invalid tmLanguage JSON: #{error.message.byteslice(0, 256)}"
71
+ end
72
+
73
+ def token_for_scope(scope)
74
+ return Rouge::Token::Tokens::Text unless scope.is_a?(String)
75
+
76
+ name = scope.split.first.to_s.downcase
77
+ case name
78
+ when /\Ainvalid(?:\.|\z)/ then Rouge::Token::Tokens::Error
79
+ when /\Acomment(?:\.|\z)/ then Rouge::Token::Tokens::Comment
80
+ when /\A(?:string\.)?(?:regexp|regex)(?:\.|\z)/ then Rouge::Token::Tokens::Literal::String::Regex
81
+ when /\Aconstant\.character\.escape(?:\.|\z)/ then Rouge::Token::Tokens::Literal::String::Escape
82
+ when /\Astring(?:\.|\z)/ then Rouge::Token::Tokens::Literal::String
83
+ when /\Aconstant\.numeric(?:\.|\z)/ then Rouge::Token::Tokens::Literal::Number
84
+ when /\Aconstant\.language(?:\.|\z)/ then Rouge::Token::Tokens::Keyword::Constant
85
+ when /\Akeyword(?:\.|\z)/ then Rouge::Token::Tokens::Keyword
86
+ when /\Astorage\.type(?:\.|\z)/ then Rouge::Token::Tokens::Keyword::Type
87
+ when /\Astorage(?:\.|\z)/ then Rouge::Token::Tokens::Keyword::Declaration
88
+ when /\Aentity\.name\.function(?:\.|\z)/ then Rouge::Token::Tokens::Name::Function
89
+ when /\A(?:entity\.name\.(?:class|type)|support\.class)(?:\.|\z)/ then Rouge::Token::Tokens::Name::Class
90
+ when /\Aentity\.other\.attribute-name(?:\.|\z)/ then Rouge::Token::Tokens::Name::Attribute
91
+ when /\Asupport\.function(?:\.|\z)/ then Rouge::Token::Tokens::Name::Builtin
92
+ when /\Avariable(?:\.|\z)/ then Rouge::Token::Tokens::Name::Variable
93
+ when /\Apunctuation(?:\.|\z)/ then Rouge::Token::Tokens::Punctuation
94
+ when /\Amarkup\.heading(?:\.|\z)/ then Rouge::Token::Tokens::Generic::Heading
95
+ when /\Amarkup\.bold(?:\.|\z)/ then Rouge::Token::Tokens::Generic::Strong
96
+ when /\Amarkup\.italic(?:\.|\z)/ then Rouge::Token::Tokens::Generic::Emph
97
+ when /\A(?:constant|support\.constant)(?:\.|\z)/ then Rouge::Token::Tokens::Name::Constant
98
+ when /\Aentity\.name(?:\.|\z)/ then Rouge::Token::Tokens::Name
99
+ else Rouge::Token::Tokens::Text
100
+ end
101
+ end
102
+
103
+ class Compiler
104
+ def initialize
105
+ @rule_count = 0
106
+ end
107
+
108
+ def compile(value)
109
+ object(value, "tmLanguage root")
110
+ unknown = value.keys - TOP_KEYS
111
+ raise GrammarError, "unsupported tmLanguage key #{unknown.first.inspect}" unless unknown.empty?
112
+
113
+ scope = string(value["scopeName"], "scopeName")
114
+ patterns = compile_patterns(value.fetch("patterns", []), 0)
115
+ source_repository = value.fetch("repository", {})
116
+ object(source_repository, "repository")
117
+ repository = source_repository.to_h do |name, rule|
118
+ raise GrammarError, "invalid repository name" unless name.is_a?(String) && !name.empty?
119
+
120
+ [name.freeze, compile_rule(rule, 1)]
121
+ end.freeze
122
+ validate_includes(patterns, repository)
123
+ repository.each_value { |rule| validate_includes([rule], repository) }
124
+ tag = scope.split(".").last.to_s.gsub(/[^a-zA-Z0-9_+.-]/, "_")
125
+ raise GrammarError, "scopeName does not contain a usable lexer tag" if tag.empty?
126
+
127
+ Definition.new(scope_name: scope.freeze, tag: tag.downcase.freeze,
128
+ patterns: patterns, repository: repository).freeze
129
+ end
130
+
131
+ private
132
+
133
+ def compile_patterns(value, depth)
134
+ raise GrammarError, "patterns must be an Array" unless value.is_a?(Array)
135
+ raise ResourceLimitError, "tmLanguage nesting exceeds #{MAX_DEPTH}" if depth >= MAX_DEPTH
136
+
137
+ value.map { |rule| compile_rule(rule, depth + 1) }.freeze
138
+ end
139
+
140
+ def compile_rule(value, depth)
141
+ object(value, "grammar rule")
142
+ unsupported = value.keys & UNSUPPORTED_RULE_KEYS
143
+ raise GrammarError, "unsupported grammar rule key #{unsupported.first.inspect}" unless unsupported.empty?
144
+ unknown = value.keys - RULE_KEYS
145
+ raise GrammarError, "unsupported grammar rule key #{unknown.first.inspect}" unless unknown.empty?
146
+ @rule_count += 1
147
+ raise ResourceLimitError, "tmLanguage exceeds #{MAX_RULES} rules" if @rule_count > MAX_RULES
148
+
149
+ kinds = %w[include match begin].select { |key| value.key?(key) }
150
+ raise GrammarError, "grammar rule has incompatible operations" if kinds.length > 1
151
+
152
+ case kinds.first
153
+ when "include" then compile_include(value)
154
+ when "match" then compile_match(value)
155
+ when "begin" then compile_begin(value, depth)
156
+ else
157
+ raise GrammarError, "grammar rule must have one operation" unless value.key?("patterns")
158
+
159
+ compile_group(value, depth)
160
+ end
161
+ end
162
+
163
+ def compile_include(value)
164
+ target = string(value["include"], "include")
165
+ valid = %w[$self $base].include?(target) || target.match?(/\A#[^#\s]+\z/)
166
+ raise GrammarError, "external grammar include #{target.inspect} is unsupported" unless valid
167
+ raise GrammarError, "include rule has incompatible fields" unless (value.keys - %w[include comment]).empty?
168
+
169
+ IncludeRule.new(target: target.freeze).freeze
170
+ end
171
+
172
+ def compile_match(value)
173
+ allowed = %w[match captures name comment]
174
+ raise GrammarError, "match rule has incompatible fields" unless (value.keys - allowed).empty?
175
+
176
+ MatchRule.new(regexp: compile_regexp(value["match"]), token: optional_token(value["name"]),
177
+ captures: compile_captures(value["captures"])).freeze
178
+ end
179
+
180
+ def compile_begin(value, depth)
181
+ raise GrammarError, "begin rule requires end" unless value.key?("end")
182
+
183
+ regexp = compile_regexp(value["begin"])
184
+ ending = string(value["end"], "end")
185
+ compiled_ending = validate_regexp(dynamic_source(ending), "end")
186
+ shared = value["captures"]
187
+ BeginRule.new(regexp: regexp, end_source: ending.freeze, ending: compiled_ending,
188
+ dynamic_end: ending.match?(/(?<!\\)\\[1-9]/), token: optional_token(value["name"]),
189
+ content_token: optional_token(value["contentName"]),
190
+ begin_captures: compile_captures(value["beginCaptures"] || shared),
191
+ end_captures: compile_captures(value["endCaptures"] || shared),
192
+ patterns: compile_patterns(value.fetch("patterns", []), depth),
193
+ apply_end_last: boolean(value.fetch("applyEndPatternLast", false), "applyEndPatternLast")).freeze
194
+ end
195
+
196
+ def compile_group(value, depth)
197
+ raise GrammarError, "pattern group has incompatible fields" unless (value.keys - %w[patterns comment]).empty?
198
+
199
+ GroupRule.new(patterns: compile_patterns(value["patterns"], depth)).freeze
200
+ end
201
+
202
+ def compile_captures(value)
203
+ return {}.freeze if value.nil?
204
+ object(value, "captures")
205
+
206
+ value.to_h do |index, capture|
207
+ raise GrammarError, "capture keys must be decimal indexes" unless index.is_a?(String) && index.match?(/\A\d{1,3}\z/)
208
+ object(capture, "capture")
209
+ unknown = capture.keys - %w[name comment]
210
+ raise GrammarError, "unsupported capture key #{unknown.first.inspect}" unless unknown.empty?
211
+
212
+ [Integer(index, 10), scope_token(string(capture["name"], "capture name"))]
213
+ end.freeze
214
+ end
215
+
216
+ def compile_regexp(value)
217
+ source = string(value, "match")
218
+ validate_regexp(source, "match")
219
+ end
220
+
221
+ def validate_regexp(source, field)
222
+ raise ResourceLimitError, "#{field} regex exceeds #{MAX_REGEX_BYTES} bytes" if source.bytesize > MAX_REGEX_BYTES
223
+
224
+ Regexp.new(source)
225
+ rescue RegexpError => error
226
+ raise GrammarError, "invalid #{field} regex: #{error.message.byteslice(0, 256)}"
227
+ end
228
+
229
+ def dynamic_source(source)
230
+ source.gsub(/(?<!\\)\\[1-9]/, "x")
231
+ end
232
+
233
+ def validate_includes(patterns, repository, seen = {})
234
+ patterns.each do |rule|
235
+ if rule.is_a?(IncludeRule) && rule.target.start_with?("#")
236
+ name = rule.target.delete_prefix("#")
237
+ raise GrammarError, "unknown repository include #{rule.target.inspect}" unless repository.key?(name)
238
+ elsif rule.is_a?(BeginRule) || rule.is_a?(GroupRule)
239
+ next if seen[rule.object_id]
240
+
241
+ seen[rule.object_id] = true
242
+ validate_includes(rule.patterns, repository, seen)
243
+ end
244
+ end
245
+ end
246
+
247
+ def optional_token(value)
248
+ value.nil? ? nil : scope_token(string(value, "scope name"))
249
+ end
250
+
251
+ def scope_token(value)
252
+ Grammar.__send__(:token_for_scope, value)
253
+ end
254
+
255
+ def object(value, name)
256
+ raise GrammarError, "#{name} must be an object" unless value.is_a?(Hash) && value.keys.all? { |key| key.is_a?(String) }
257
+ end
258
+
259
+ def string(value, name)
260
+ raise GrammarError, "#{name} must be a nonempty String" unless value.is_a?(String) && !value.empty?
261
+
262
+ value
263
+ end
264
+
265
+ def boolean(value, name)
266
+ raise GrammarError, "#{name} must be true or false" unless value == true || value == false
267
+
268
+ value
269
+ end
270
+ end
271
+ private_constant :Compiler
272
+ private_class_method :parse, :token_for_scope
273
+ end
274
+ end
@@ -51,6 +51,17 @@ module Antares
51
51
  indexes.map { |index| @tokens.fetch(index) { plain(index) } }
52
52
  end
53
53
 
54
+ def structure
55
+ @structure ||= Structure.build(
56
+ language: @template.class.tag,
57
+ lines: @lines,
58
+ line_count: @line_count,
59
+ tokens_for: method(:tokens_for),
60
+ tokens_in: method(:tokens_in),
61
+ stabilize: method(:stabilize_structure)
62
+ )
63
+ end
64
+
54
65
  # Provider contents must already reflect this line edit. Indices are zero based.
55
66
  def edit(from_line:, removed:, inserted:)
56
67
  values = [from_line, removed, inserted]
@@ -58,10 +69,16 @@ module Antares
58
69
  raise RangeError, "edit outside previous document" unless from_line <= @count && from_line + removed <= @count
59
70
  updated_count = count
60
71
  raise ArgumentError, "line_count disagrees with edit" unless updated_count == @count - removed + inserted
72
+ separator_line = if from_line.positive? &&
73
+ ((from_line == @count && inserted.positive?) || (from_line + removed == @count && inserted.zero? && removed.positive?))
74
+ from_line - 1
75
+ end
76
+ restart_from = separator_line || from_line
77
+ source_edit = @source_edit ? nil : (@source && [@source, @offsets, @count, from_line, removed, inserted])
61
78
  @last_scanned_lines = 0
62
79
  if strategy == :incremental
63
80
  @tokens.delete_if { |line, _| line >= frontier } if @old_fingerprints
64
- start = @checkpoints.keys.select { |line| line <= from_line && line <= frontier }.max || 0
81
+ start = @checkpoints.keys.select { |line| line <= restart_from && line <= frontier }.max || 0
65
82
  restart = @checkpoints.fetch(start)
66
83
  shift_cache(@tokens, from_line, removed, inserted)
67
84
  shift_cache(@fingerprints, from_line, removed, inserted)
@@ -79,7 +96,10 @@ module Antares
79
96
  @frontier = 0
80
97
  end
81
98
  @count = updated_count
99
+ @source_edit = source_edit
82
100
  @source = @offsets = @driver = nil
101
+ @structure&.edit(from_line: from_line, removed: removed, inserted: inserted)
102
+ @structure&.edit(from_line: separator_line, removed: 1, inserted: 1) if separator_line
83
103
  self
84
104
  end
85
105
 
@@ -146,22 +166,70 @@ module Antares
146
166
  def source_line(index)
147
167
  value = @lines.call(index)
148
168
  raise TypeError, "line provider must return a String" unless value.is_a?(String)
149
- raise EncodingError, "line provider must return valid UTF-8" unless value.valid_encoding? && [Encoding::UTF_8, Encoding::US_ASCII].include?(value.encoding)
150
- raise ArgumentError, "line provider returned multiple logical lines" if value.count("\n") > 1 || (value.include?("\n") && !value.end_with?("\n"))
169
+ encoding = value.encoding
170
+ unless value.valid_encoding? && (encoding == Encoding::UTF_8 || encoding == Encoding::US_ASCII)
171
+ raise EncodingError, "line provider must return valid UTF-8"
172
+ end
173
+ newline = value.index("\n")
174
+ raise ArgumentError, "line provider returned multiple logical lines" if newline && newline != value.length - 1
151
175
  # Providers may omit separators, but an empty final line remains empty.
152
- index < @count - 1 && !value.end_with?("\n") ? value + "\n" : value
176
+ index < @count - 1 && newline.nil? ? value + "\n" : value
153
177
  end
154
178
 
155
179
  def build_source
156
180
  return if @source
157
- source = +""
158
- offsets = [0]
159
- @count.times do |index|
181
+ if @source_edit
182
+ build_edited_source
183
+ @source_edit = nil
184
+ return
185
+ end
186
+ parts = Array.new(@count)
187
+ offsets = Array.new(@count + 1, 0)
188
+ bytes = 0
189
+ index = 0
190
+ while index < @count
191
+ line = source_line(index)
192
+ line_bytes = line.bytesize
193
+ raise ResourceLimitError, "line exceeds #{@max_line_bytes} bytes" if line_bytes > @max_line_bytes
194
+ bytes += line_bytes
195
+ raise ResourceLimitError, "document exceeds #{@max_bytes} bytes" if bytes > @max_bytes
196
+ parts[index] = line
197
+ offsets[index + 1] = bytes
198
+ index += 1
199
+ end
200
+ @source, @offsets = parts.join.freeze, offsets.freeze
201
+ end
202
+
203
+ def build_edited_source
204
+ old_source, old_offsets, old_count, from_line, removed, inserted = @source_edit
205
+ first = [from_line - 1, 0].max
206
+ old_suffix = from_line + removed
207
+ new_suffix = from_line + inserted
208
+ prefix_bytes = old_offsets.fetch(first)
209
+ suffix_bytes = old_offsets.fetch(old_suffix)
210
+ offsets = old_offsets[0..first]
211
+ middle = []
212
+ bytes = prefix_bytes
213
+ index = first
214
+ while index < new_suffix
160
215
  line = source_line(index)
161
- raise ResourceLimitError, "line exceeds #{@max_line_bytes} bytes" if line.bytesize > @max_line_bytes
162
- raise ResourceLimitError, "document exceeds #{@max_bytes} bytes" if source.bytesize + line.bytesize > @max_bytes
163
- source << line
164
- offsets << source.bytesize
216
+ line_bytes = line.bytesize
217
+ raise ResourceLimitError, "line exceeds #{@max_line_bytes} bytes" if line_bytes > @max_line_bytes
218
+ bytes += line_bytes
219
+ middle << line
220
+ offsets << bytes
221
+ index += 1
222
+ end
223
+ total = bytes + old_source.bytesize - suffix_bytes
224
+ raise ResourceLimitError, "document exceeds #{@max_bytes} bytes" if total > @max_bytes
225
+
226
+ source = String.new(capacity: total)
227
+ source << old_source.byteslice(0, prefix_bytes) << middle.join << old_source.byteslice(suffix_bytes..)
228
+ delta = bytes - suffix_bytes
229
+ boundary = old_suffix + 1
230
+ while boundary <= old_count
231
+ offsets << old_offsets.fetch(boundary) + delta
232
+ boundary += 1
165
233
  end
166
234
  @source, @offsets = source.freeze, offsets.freeze
167
235
  end
@@ -207,8 +275,9 @@ module Antares
207
275
 
208
276
  def advance_window(first, last)
209
277
  Timeout.timeout(@max_seconds) { window_tokens(first, last) }
210
- rescue Timeout::Error
211
- @fallback_reason = "window lexing exceeded #{@max_seconds} seconds"
278
+ rescue Timeout::Error, ResourceLimitError => error
279
+ @fallback_reason = error.is_a?(Timeout::Error) ?
280
+ "window lexing exceeded #{@max_seconds} seconds" : error.message
212
281
  (first..last).each { |index| @tokens[index] = plain(index) }
213
282
  @frontier = last + 1
214
283
  end
@@ -260,6 +329,7 @@ module Antares
260
329
 
261
330
  def fallback!(strategy, reason)
262
331
  @strategy, @fallback_reason = strategy, reason
332
+ @source_edit = nil
263
333
  @driver = @source = @offsets = nil
264
334
  @tokens.clear
265
335
  @checkpoints.clear
@@ -270,6 +340,10 @@ module Antares
270
340
 
271
341
  def shift_cache(cache, first, removed, inserted)
272
342
  delta = inserted - removed
343
+ if delta.zero?
344
+ removed.times { |offset| cache.delete(first + offset) }
345
+ return
346
+ end
273
347
  shifted = {}
274
348
  cache.each do |line, value|
275
349
  if line < first
@@ -280,5 +354,11 @@ module Antares
280
354
  end
281
355
  cache.replace(shifted)
282
356
  end
357
+
358
+ def stabilize_structure(from_line)
359
+ return 0 if @count.zero?
360
+ advance(until_line: @count - 1, from_line: from_line)
361
+ strategy == :incremental && @driver ? [@driver.line, @count].min : @count
362
+ end
283
363
  end
284
364
  end