spellr 0.5.0 → 0.5.1

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.
@@ -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