spellr 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stats
4
+ module_function
5
+
6
+ extend Math
7
+
4
8
  def mean(values, &block)
5
9
  return 0 if values.empty?
6
10
 
@@ -9,25 +13,40 @@ module Stats
9
13
 
10
14
  def min(values, &block)
11
15
  return 0 if values.empty?
16
+ return values.min unless block_given?
12
17
 
13
- block ||= :itself.to_proc
14
- block.call(values.min_by(&block))
18
+ yield values.min_by(&block)
15
19
  end
16
20
 
17
21
  def max(values, &block)
18
22
  return 0 if values.empty?
23
+ return values.max unless block_given?
19
24
 
20
- block ||= :itself.to_proc
21
- block.call(values.max_by(&block))
25
+ yield values.max_by(&block)
22
26
  end
23
27
 
24
- def variance(values, &block)
28
+ def variance(values, &block) # rubocop:disable Metrics/MethodLength
25
29
  return 0 if values.empty?
26
30
 
27
- values.sum { |sample| (mean(values, &block) - (block ? block.call(sample) : sample))**2 }.to_f / values.length
31
+ mean = mean(values, &block)
32
+ values.sum do |value|
33
+ value = yield value if block_given?
34
+ (mean - value)**2
35
+ end.to_f / values.length
28
36
  end
29
37
 
30
38
  def standard_deviation(values, &block)
31
- Math.sqrt(variance(values, &block))
39
+ sqrt(variance(values, &block))
40
+ end
41
+
42
+ def gaussian_probability(value, standard_deviation:, mean:, variance:)
43
+ # deal with the edge case of a 0 standard deviation
44
+ if standard_deviation == 0
45
+ return mean == value ? 1.0 : 0.0
46
+ end
47
+
48
+ # calculate the gaussian probability
49
+ exp = -((value - mean)**2) / (2 * variance)
50
+ (1.0 / sqrt(2 * Math::PI * variance)) * (Math::E**exp)
32
51
  end
33
52
  end
@@ -7,52 +7,45 @@ module Spellr
7
7
  attr_reader :name
8
8
  attr_reader :key
9
9
 
10
- def initialize(name, # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
11
- key: name[0],
12
- generate: nil,
13
- only: [],
14
- includes: [],
15
- description: '',
16
- hashbangs: [],
17
- locale: [])
18
- unless only.empty?
19
- warn <<~WARNING
20
- \e[33mSpellr: `only:` language yaml key with a list of fnmatch rules is deprecated.
21
- Please use `includes:` instead, which uses gitignore-inspired rules.
22
- see github.com/robotdana/fast_ignore#using-an-includes-list for details\e[0m
23
- WARNING
24
- end
25
-
26
- if generate
27
- warn <<~WARNING
28
- \e[33mSpellr: `generate:` and generation is now deprecated. Choose the language
29
- using the key `locale:` (any of US,AU,CA,GB,GBz,GBs as a string or array).\e[0m
30
- WARNING
31
- end
32
-
10
+ def initialize(name, key: name[0], includes: [], hashbangs: [], locale: [])
33
11
  @name = name
34
12
  @key = key
35
- @description = description
36
- @includes = only + includes
13
+ @includes = includes
37
14
  @hashbangs = hashbangs
38
15
  @locales = Array(locale)
39
16
  end
40
17
 
41
18
  def matches?(file)
42
- return true if @includes.empty?
19
+ matches_includes?(file) || matches_hashbangs?(file)
20
+ end
43
21
 
44
- return true if fast_ignore.allowed?(file.to_s)
22
+ def wordlists
23
+ default_wordlists.select(&:exist?)
24
+ end
45
25
 
46
- file = Spellr::File.wrap(file)
47
- return true if !@hashbangs.empty? && file.hashbang && @hashbangs.any? { |h| file.hashbang.include?(h) }
26
+ def project_wordlist
27
+ @project_wordlist ||= Spellr::Wordlist.new(
28
+ Pathname.pwd.join('.spellr_wordlists', "#{name}.txt"),
29
+ name: name
30
+ )
48
31
  end
49
32
 
50
- def fast_ignore
51
- @fast_ignore ||= FastIgnore.new(include_rules: @includes, gitignore: false)
33
+ private
34
+
35
+ def matches_hashbangs?(file)
36
+ return @includes.empty? if @hashbangs.empty?
37
+
38
+ file = Spellr::File.wrap(file)
39
+ return unless file.hashbang
40
+
41
+ @hashbangs.any? { |h| file.hashbang.include?(h) }
52
42
  end
53
43
 
54
- def wordlists
55
- default_wordlists.select(&:exist?)
44
+ def matches_includes?(file)
45
+ return @hashbangs.empty? if @includes.empty?
46
+
47
+ @fast_ignore ||= FastIgnore.new(include_rules: @includes, gitignore: false)
48
+ @fast_ignore.allowed?(file.to_s)
56
49
  end
57
50
 
58
51
  def gem_wordlist
@@ -61,13 +54,6 @@ module Spellr
61
54
  )
62
55
  end
63
56
 
64
- def project_wordlist
65
- @project_wordlist ||= Spellr::Wordlist.new(
66
- Pathname.pwd.join('.spellr_wordlists', "#{name}.txt"),
67
- name: name
68
- )
69
- end
70
-
71
57
  def locale_wordlists
72
58
  @locale_wordlists ||= @locales.map do |locale|
73
59
  Spellr::Wordlist.new(
@@ -76,8 +62,6 @@ module Spellr
76
62
  end
77
63
  end
78
64
 
79
- private
80
-
81
65
  def load_wordlists(name, paths)
82
66
  wordlists = paths + default_wordlist_paths(name)
83
67
 
@@ -87,8 +71,8 @@ module Spellr
87
71
  def default_wordlists
88
72
  [
89
73
  gem_wordlist,
90
- project_wordlist,
91
- *locale_wordlists
74
+ *locale_wordlists,
75
+ project_wordlist
92
76
  ]
93
77
  end
94
78
  end
@@ -2,28 +2,34 @@
2
2
 
3
3
  module Spellr
4
4
  class LineLocation
5
- attr_reader :file
6
5
  attr_reader :line_number
7
6
  attr_reader :char_offset
8
7
  attr_reader :byte_offset
9
8
 
10
9
  def initialize(file = '[String]', line_number = 1, char_offset: 0, byte_offset: 0)
11
- @file = file
10
+ @filename = file
12
11
  @line_number = line_number
13
12
  @char_offset = char_offset
14
13
  @byte_offset = byte_offset
15
14
  end
16
15
 
17
16
  def to_s
18
- "#{relative_file_name}:#{line_number}"
17
+ "#{file_relative_path}:#{line_number}"
19
18
  end
20
19
 
21
- def file_name
22
- file.respond_to?(:to_path) ? file.to_path : file
20
+ def file_relative_path
21
+ file.relative_path
23
22
  end
24
23
 
25
- def relative_file_name
26
- Pathname.new(file_name).relative_path_from(Pathname.pwd)
24
+ def file
25
+ @file ||= Spellr::File.wrap(@filename)
26
+ end
27
+
28
+ def advance(line)
29
+ LineLocation.new(@filename,
30
+ line_number + 1,
31
+ char_offset: char_offset + line.length,
32
+ byte_offset: byte_offset + line.bytesize)
27
33
  end
28
34
  end
29
35
  end
@@ -5,57 +5,62 @@ require_relative '../spellr'
5
5
  require_relative 'column_location'
6
6
  require_relative 'token'
7
7
  require_relative 'key_tuner/naive_bayes'
8
+ require_relative 'token_regexps'
8
9
 
9
10
  module Spellr
10
- class LineTokenizer < StringScanner # rubocop:disable Metrics/ClassLength
11
+ class LineTokenizer < StringScanner
11
12
  attr_reader :line
12
13
  attr_accessor :disabled
13
14
  alias_method :disabled?, :disabled
14
- attr_accessor :skip_uri
15
- alias_method :skip_uri?, :skip_uri
16
15
  attr_accessor :skip_key
17
16
  alias_method :skip_key?, :skip_key
18
17
 
19
- def initialize(*line, skip_uri: true, skip_key: true)
20
- @line = Spellr::Token.wrap(line.first)
21
- @skip_uri = skip_uri
18
+ include TokenRegexps
19
+
20
+ def initialize(line, skip_key: true)
21
+ @line = line
22
22
  @skip_key = skip_key
23
23
 
24
24
  super(@line.to_s)
25
25
  end
26
26
 
27
27
  def string=(line)
28
- @line = Token.wrap(line)
28
+ @line = line
29
29
  super(@line.to_s)
30
30
  end
31
31
 
32
32
  def each_term
33
33
  until eos?
34
34
  term = next_term
35
- next unless term
36
- next if disabled?
35
+ next if !term || disabled?
37
36
 
38
37
  yield term
39
38
  end
40
39
  end
41
40
 
42
- def each_token
41
+ def each_token(skip_term_proc: nil) # rubocop:disable Metrics/MethodLength
43
42
  until eos?
44
43
  term = next_term
45
44
  next unless term
46
- next if disabled?
45
+ next if disabled? || skip_term_proc&.call(term)
47
46
 
48
47
  yield Token.new(term, line: line, location: column_location(term))
49
48
  end
50
49
  end
51
50
 
51
+ # jump to character-aware position
52
+ # TODO: handle jump backward
53
+ def charpos=(new_charpos)
54
+ skip(/.{#{new_charpos - charpos}}/m)
55
+ end
56
+
52
57
  private
53
58
 
54
59
  def column_location(term)
55
60
  ColumnLocation.new(
56
61
  byte_offset: pos - term.bytesize,
57
62
  char_offset: charpos - term.length,
58
- line_location: line.location.line_location
63
+ **(line.respond_to?(:location) ? { line_location: line.location.line_location } : {})
59
64
  )
60
65
  end
61
66
 
@@ -64,23 +69,10 @@ module Spellr
64
69
  end
65
70
 
66
71
  def next_term
67
- if skip_nonwords_and_flags
68
- nil
69
- else
70
- scan_term
71
- end
72
- end
72
+ return if skip_nonwords_and_flags
73
73
 
74
- # [Word], [Word]Word [Word]'s [Wordn't]
75
- TITLE_CASE_RE = /[[:upper:]][[:lower:]]+(?:['’][[:lower:]]+(?<!['’]s))*/.freeze
76
- # [WORD] [WORD]Word [WORDN'T] [WORD]'S [WORD]'s [WORD]s
77
- UPPER_CASE_RE = /[[:upper:]]+(?:['’][[:upper:]]+(?<!['’][Ss]))*(?:(?![[:lower:]])|(?=s(?![[:lower:]])))/.freeze
78
- # [word] [word]'s [wordn't]
79
- LOWER_CASE_RE = /[[:lower:]]+(?:['’][[:lower:]]+(?<!['’]s))*/.freeze
80
- # for characters in [:alpha:] that aren't in [:lower:] or [:upper:] e.g. Arabic
81
- OTHER_CASE_RE = /(?:[[:alpha:]](?<![[:lower:][:upper:]]))+/.freeze
82
-
83
- TERM_RE = Regexp.union(TITLE_CASE_RE, UPPER_CASE_RE, LOWER_CASE_RE, OTHER_CASE_RE)
74
+ scan_term
75
+ end
84
76
 
85
77
  def scan_term
86
78
  term = scan(TERM_RE)
@@ -88,126 +80,35 @@ module Spellr
88
80
  return term if term && term.length >= Spellr.config.word_minimum_length
89
81
  end
90
82
 
91
- NOT_EVEN_NON_WORDS_RE = %r{[^[:alpha:]/%#0-9\\]+}.freeze # everything not covered by more specific skips/scans
92
- LEFTOVER_NON_WORD_BITS_RE = %r{[/%#\\]|\d+}.freeze # e.g. a / not starting //a-url.com
93
- HEX_RE = /(?:#(?:\h{6}|\h{3})|0x\h+)(?![[:alpha:]])/.freeze
94
- SHELL_COLOR_ESCAPE_RE = /\\(?:e|0?33)\[\d+(;\d+)*m/.freeze
95
- PUNYCODE_RE = /xn--[a-v0-9\-]+(?:[[:alpha:]])/.freeze
96
- BACKSLASH_ESCAPE_RE = /\\[a-zA-Z]/.freeze # TODO: hex escapes e.g. \xAA. TODO: language aware escapes
97
- REPEATED_SINGLE_LETTERS_RE = /(?:([[:alpha:]])\1+)(?![[:alpha:]])/.freeze # e.g. xxxxxxxx (it's not a word)
98
- URL_ENCODED_ENTITIES_RE = /%[0-8A-F]{2}/.freeze
99
- # There's got to be a better way of writing this
100
- SEQUENTIAL_LETTERS_RE = /a(?:b(?:c(?:d(?:e(?:f(?:g(?:h(?:i(?:j(?:k(?:l(?:m(?:n(?:o(?:p(?:q(?:r(?:s(?:t(?:u(?:v(?:w(?:x(?:yz?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?(?![[:alpha:]])/i.freeze # rubocop:disable Metrics/LineLength
101
-
102
- # I didn't want to do this myself. BUT i need something to heuristically match on, and it's difficult
103
- URL_SCHEME = '(//|https?://|s?ftp://|mailto:)'
104
- URL_USERINFO = '([[:alnum:]]+(?::[[:alnum:]]+)?@)'
105
- URL_HOSTNAME = '((?:[[:alnum:]-]+(?:\\\\?\\.[[:alnum:]-]+)+|localhost|\\d{1,3}(?:\\.\\d{1,3}){3}))'
106
- URL_PORT = '(:\\d+)'
107
- URL_PATH = '(/(?:[[:alnum:]=@!$&\\-/._\\\\]|%\h{2})+)'
108
- URL_QUERY = '(\\?(?:[[:alnum:]=!$\\-/.\\\\]|%\\h{2})+(?:&(?:[[:alnum:]=!$\\-/.\\\\]|%\\h{2})+)*)'
109
- URL_FRAGMENT = '(\\#(?:[[:alnum:]=!$&\\-/.\\\\]|%\\h{2})+)'
110
- URL_RE = /
111
- (?:
112
- #{URL_SCHEME}#{URL_USERINFO}?#{URL_HOSTNAME}#{URL_PORT}?#{URL_PATH}?
113
- |
114
- #{URL_SCHEME}?#{URL_USERINFO}#{URL_HOSTNAME}#{URL_PORT}?#{URL_PATH}?
115
- |
116
- #{URL_SCHEME}?#{URL_USERINFO}?#{URL_HOSTNAME}#{URL_PORT}?#{URL_PATH}
117
- )
118
- #{URL_QUERY}?#{URL_FRAGMENT}?
119
- /x.freeze
120
-
121
- KNOWN_KEY_PATTERNS_RE = %r{(
122
- SG\.[\w\-]{22}\.[\w\-]{43} | # sendgrid
123
- prg-\h{8}-\h{4}-\h{4}-\h{4}-\h{12} | # hyperwallet
124
- GTM-[A-Z0-9]{7} | # google tag manager
125
- sha1-[A-Za-z0-9=+/]{28} |
126
- sha512-[A-Za-z0-9=+/]{88} |
127
- data:[a-z/;0-9\-]+;base64,[A-Za-z0-9+/]+=*(?![[:alnum:]])
128
- )}x.freeze
129
-
130
- SKIPS = Regexp.union(
131
- NOT_EVEN_NON_WORDS_RE,
132
- SHELL_COLOR_ESCAPE_RE,
133
- BACKSLASH_ESCAPE_RE,
134
- URL_ENCODED_ENTITIES_RE,
135
- HEX_RE,
136
- URL_RE, # 2%
137
- KNOWN_KEY_PATTERNS_RE
138
- ).freeze
139
-
140
- AFTER_KEY_SKIPS = Regexp.union(
141
- LEFTOVER_NON_WORD_BITS_RE,
142
- REPEATED_SINGLE_LETTERS_RE,
143
- SEQUENTIAL_LETTERS_RE
144
- )
145
-
146
83
  def skip_nonwords
147
- skip(SKIPS) ||
148
- skip_key_heuristically || # 5%
149
- skip(AFTER_KEY_SKIPS)
84
+ skip(SKIPS) || skip_key_heuristically || skip(AFTER_KEY_SKIPS)
150
85
  end
151
86
 
152
- KEY_RE = %r{[A-Za-z0-9]([A-Za-z0-9+/\-_]*)=*(?![[:alnum:]])}.freeze
153
- N = NaiveBayes.new
154
- def skip_key_heuristically # rubocop:disable Metrics/MethodLength
155
- return unless scan(KEY_RE)
156
- # I've come across some large base64 strings by this point they're definitely base64.
157
- return true if matched.length > 200
158
-
159
- if key_roughly?(matched)
160
- if N.key?(matched)
161
- true
162
- else
163
- unscan
164
- false
165
- end
166
- else
167
- unscan
168
- false
169
- end
170
- end
87
+ def skip_key_heuristically
88
+ possible_key = check(POSSIBLE_KEY_RE)
171
89
 
172
- # this is in a method because the minimum word length stuff was throwing it off
173
- # TODO: move to config maybe?
174
- def min_alpha_re
175
- /(?:
176
- [A-Z][a-z]{#{Spellr.config.word_minimum_length - 1}}
177
- |
178
- [a-z]{#{Spellr.config.word_minimum_length}}
179
- |
180
- [A-Z]{#{Spellr.config.word_minimum_length}}
181
- )/x.freeze
182
- end
183
- ALPHA_SEP_RE = '[A-Za-z][A-Za-z\\-_/+]*'
184
- NUM_SEP_RE = '\\d[\\d\\-_/+]*'
185
- THREE_CHUNK_RE = /^(?:
186
- #{ALPHA_SEP_RE}#{NUM_SEP_RE}#{ALPHA_SEP_RE}
187
- |
188
- #{NUM_SEP_RE}#{ALPHA_SEP_RE}#{NUM_SEP_RE}
189
- )/x.freeze
190
- def key_roughly?(matched)
191
- return unless matched.length >= Spellr.config.key_minimum_length
192
- return unless matched.match?(THREE_CHUNK_RE)
193
- return unless matched.match?(min_alpha_re) # or there's no point
194
-
195
- true
90
+ return unless possible_key
91
+ return unless key?(possible_key)
92
+
93
+ self.pos += possible_key.bytesize
196
94
  end
197
95
 
198
- # jump to character-aware position
199
- def charpos=(new_charpos)
200
- skip(/.{#{new_charpos - charpos}}/m)
96
+ BAYES_KEY_HEURISTIC = NaiveBayes.new
97
+ def key?(possible_key)
98
+ # I've come across some large base64 strings by this point they're definitely base64.
99
+ return true if possible_key.length > 200
100
+ return unless possible_key.length >= Spellr.config.key_minimum_length
101
+ return unless possible_key.match?(min_alpha_re) # or there's no point
102
+
103
+ BAYES_KEY_HEURISTIC.key?(possible_key)
201
104
  end
202
105
 
203
- SPELLR_DISABLE_RE = /spellr:disable/.freeze
204
106
  def skip_and_track_disable
205
107
  return if disabled?
206
108
 
207
109
  skip(SPELLR_DISABLE_RE) && self.disabled = true
208
110
  end
209
111
 
210
- SPELLR_ENABLE_RE = /spellr:enable/.freeze
211
112
  def skip_and_track_enable
212
113
  return unless disabled?
213
114
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spellr
4
+ class Output
5
+ attr_reader :exit_code
6
+
7
+ def initialize
8
+ @exit_code = 0
9
+ end
10
+
11
+ def stdin
12
+ @stdin ||= $stdin
13
+ end
14
+
15
+ def stdout
16
+ @stdout ||= $stdout
17
+ end
18
+
19
+ def stderr
20
+ @stderr ||= $stderr
21
+ end
22
+
23
+ def stdout?
24
+ defined?(@stdout)
25
+ end
26
+
27
+ def stderr?
28
+ defined?(@stderr)
29
+ end
30
+
31
+ def counts
32
+ @counts ||= Hash.new(0)
33
+ end
34
+
35
+ def exit_code=(value)
36
+ @exit_code = value unless value.zero?
37
+ end
38
+
39
+ def increment(counter)
40
+ counts[counter] += 1
41
+ end
42
+
43
+ def puts(str)
44
+ stdout.puts(str)
45
+ end
46
+
47
+ def warn(str)
48
+ stderr.puts(str)
49
+ end
50
+
51
+ def print(str)
52
+ stdout.print(str)
53
+ end
54
+
55
+ def <<(other) # rubocop:disable Metrics/AbcSize
56
+ self.exit_code = other.exit_code
57
+ stderr.puts other.stderr.string if other.stderr?
58
+ stdout.puts other.stdout.string if other.stdout?
59
+ counts.merge!(other.counts) { |_k, a, b| a + b }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'output'
4
+
5
+ module Spellr
6
+ class OutputStubbed < Spellr::Output
7
+ attr_accessor :exit_code
8
+
9
+ def initialize
10
+ @exit_code = 0
11
+ end
12
+
13
+ def stdin
14
+ @stdin ||= StringIO.new
15
+ end
16
+
17
+ def stdout
18
+ @stdout ||= StringIO.new
19
+ end
20
+
21
+ def stderr
22
+ @stderr ||= StringIO.new
23
+ end
24
+
25
+ def marshal_dump # rubocop:disable Metrics/MethodLength
26
+ {
27
+ exit_code: exit_code,
28
+ counts: @counts,
29
+ stdin: @stdin&.string,
30
+ stdin_pos: @stdin&.pos,
31
+ stdout: @stdout&.string,
32
+ stdout_pos: @stdout&.pos,
33
+ stderr: @stderr&.string,
34
+ stderr_pos: @stderr&.pos
35
+ }
36
+ end
37
+
38
+ def marshal_load(dumped) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
39
+ if dumped[:stdin]
40
+ @stdin = StringIO.new(dumped[:stdin])
41
+ @stdin.pos = dumped[:stdin_pos]
42
+ end
43
+
44
+ if dumped[:stdout]
45
+ @stdout = StringIO.new(dumped[:stdout])
46
+ @stdout.pos = dumped[:stdout_pos]
47
+ end
48
+
49
+ if dumped[:stderr]
50
+ @stderr = StringIO.new(dumped[:stderr])
51
+ @stderr.pos = dumped[:stderr_pos]
52
+ end
53
+
54
+ @exit_code = dumped[:exit_code]
55
+ @counts = dumped[:counts]
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_reporter'
4
+
5
+ module Spellr
6
+ class QuietReporter < Spellr::BaseReporter
7
+ def output
8
+ @output ||= Spellr::OutputStubbed.new
9
+ end
10
+
11
+ def call(_token); end
12
+ end
13
+ end
@@ -1,27 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'string_format'
3
+ require_relative 'base_reporter'
4
4
 
5
5
  module Spellr
6
- class Reporter
7
- include Spellr::StringFormat
8
-
9
- attr_accessor :total
10
-
11
- def initialize
12
- @total = 0
6
+ class Reporter < Spellr::BaseReporter
7
+ def parallel?
8
+ true
13
9
  end
14
10
 
15
- def finish(checked)
11
+ def finish
16
12
  puts "\n"
17
- puts "#{pluralize 'file', checked} checked"
18
- puts "#{pluralize 'error', total} found"
13
+ puts "#{pluralize 'file', counts[:checked]} checked"
14
+ puts "#{pluralize 'error', counts[:total]} found"
19
15
  end
20
16
 
21
17
  def call(token)
22
- puts "#{aqua token.location} #{token.line.highlight(token.char_range).strip}"
18
+ super
23
19
 
24
- self.total += 1
20
+ increment(:total)
25
21
  end
26
22
  end
27
23
  end
data/lib/spellr/token.rb CHANGED
@@ -1,18 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # frozen string_literal: true
4
-
5
3
  require_relative 'column_location'
6
4
  require_relative 'string_format'
7
5
 
8
6
  class String
9
- def normalize
10
- normalize_cache[to_s]
11
- end
12
-
13
- def normalize_cache
14
- @@normalize_cache ||= Hash.new do |cache, term| # rubocop:disable Style/ClassVars # i want this shared with subclasses
15
- cache[term] = term.strip.downcase.unicode_normalize.tr('’', "'") + "\n"
7
+ def spellr_normalize
8
+ @@spellr_normalize ||= {} # rubocop:disable Style/ClassVars # I want to share this with subclasses
9
+ @@spellr_normalize.fetch(to_s) do |term|
10
+ @@spellr_normalize[term] = "#{term.strip.downcase.unicode_normalize.tr('’', "'")}\n"
16
11
  end
17
12
  end
18
13
  end
@@ -49,6 +44,11 @@ module Spellr
49
44
  )
50
45
  end
51
46
 
47
+ def line=(new_line)
48
+ @line = new_line
49
+ location.line_location = new_line.location.line_location
50
+ end
51
+
52
52
  def inspect
53
53
  "#<#{self.class.name} #{to_s.inspect} @#{location}>"
54
54
  end
@@ -61,6 +61,10 @@ module Spellr
61
61
  @byte_range ||= location.byte_offset...(location.byte_offset + bytesize)
62
62
  end
63
63
 
64
+ def file_char_range
65
+ @file_char_range ||= location.absolute_char_offset...(location.absolute_char_offset + length)
66
+ end
67
+
64
68
  def coordinates
65
69
  location.coordinates
66
70
  end
@@ -71,13 +75,7 @@ module Spellr
71
75
 
72
76
  def replace(replacement)
73
77
  @replacement = replacement
74
- ::File.open(file_name, 'r+') do |f|
75
- body = f.read
76
- body[location.absolute_char_offset...(location.absolute_char_offset + length)] = replacement
77
- f.rewind
78
- f.truncate(0)
79
- f.write(body)
80
- end
78
+ location.file.insert(replacement, file_char_range)
81
79
  end
82
80
 
83
81
  def file_name