string_hound 0.1.5

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 65019ba6e170312635d84647e62cb746d21bc71f
4
+ data.tar.gz: 01c92226b9db34dd5d91dba33b4868a1d69d686b
5
+ SHA512:
6
+ metadata.gz: 5e3deb11986693e16b4a22e2a548cda6d6f5b52a3a369988336e82961e18c29d7e04933cfb248ffee6ce327ababae73ce444c7f329da7b499971d109a4f8275d
7
+ data.tar.gz: dd17a9d6d37ad87e4f329f73fffa1fd43b507c7d55b51f7a35391fc6c9a78f0a261b2fae6bd001720a8263080668420216b0102400e29849a32fdb1d8a821390
@@ -0,0 +1,37 @@
1
+ module RegexUtils
2
+
3
+ def self.included(base)
4
+ base.class_eval do
5
+
6
+ def parse_for_strings(line)
7
+ line.scan(/["'][\w\s#\{\}]*["']/)
8
+ end
9
+
10
+ def is_erb_txt(line)
11
+ !!line.match(/<%=/).nil? && line.match(/(<%|%>)/)
12
+ end
13
+
14
+ def is_javascript(line)
15
+ !!line.match(/(\$j|\$z|function)/)
16
+ end
17
+
18
+ def find_printed_erb(line)
19
+ line.match(/<%=.*?(\n|%>)/)
20
+ end
21
+
22
+ def inline_strings(line)
23
+ if @inline && line.match(/^[\s]*(TEXT|CONTENT)/)
24
+ @inline = nil
25
+ elsif @inline || match = line.match(/(<<-TEXT|<<-CONTENT)[\s]*/)
26
+ @inline = true
27
+ match.nil? ? line : match.post_match
28
+ end
29
+ end
30
+
31
+ def find_variables(content)
32
+ content.scan(/#\{(\w*)\}/)
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,261 @@
1
+ require 'tempfile'
2
+ require 'find'
3
+ require 'nokogiri'
4
+ require 'fileutils'
5
+ require 'regex_utils'
6
+
7
+ ##
8
+ #
9
+ #
10
+ # Given a directory, StringHound recursively searches the directory
11
+ # heirarchy looking for any hardcoded strings. When found, it prints them
12
+ # to standard out in the form:
13
+ # <filename>: <line> <string value>
14
+ #
15
+ # In speak mode, Stringhound will also insert a suggested i18n conversion of
16
+ # all strings it finds into the file it finds them in, as well as
17
+ # insert the same key and translation into the default yml file
18
+ ##
19
+
20
+ class StringHound
21
+
22
+ include RegexUtils
23
+
24
+ attr_reader :view_file
25
+ attr_accessor :file, :command_speak, :interactive
26
+
27
+ class << self; attr_accessor :default_yml end
28
+ @default_yml = "config/locales/translations/admin.yml"
29
+
30
+
31
+ def initialize(dir, opts = nil)
32
+ @directory = dir
33
+ @prize = []
34
+ @command_speak = false
35
+ @interactive = opts && opts[:interactive] ? opts[:interactive] : false
36
+ end
37
+
38
+
39
+ #
40
+ # Iterates through directory and sets up
41
+ # current file to hunt through.
42
+ # Close yml_file if speak was enabled
43
+ #
44
+ def hunt
45
+ Find.find(@directory) do |f|
46
+ unless FileTest.directory?(f)
47
+ @file = File.open(f, "r")
48
+ @view_file = ['.html', '.erb'].include?(File.extname(f))
49
+ sniff
50
+ @file.close if !@file.closed?
51
+ end
52
+ end
53
+ @yml_file.close if @yml_file
54
+ end
55
+
56
+
57
+ #
58
+ # Grabs content line by line from file and
59
+ # parses it.
60
+ # If speak is enabled, cleanup associated files
61
+ # after it runs
62
+ #
63
+ def sniff
64
+ @file.each_line do |l|
65
+ taste(l)
66
+ @content_arry.each { |m| @prize << {:filename => @file.path, :line_number => @file.lineno, :value => m } }
67
+ speak(l)
68
+ end
69
+
70
+ file_cleanup
71
+ end
72
+
73
+ #
74
+ # Parse engine
75
+ #
76
+ def taste(line)
77
+ @content_arry = out = []
78
+ #Note: this is temporary, will create a new class for 'line' to handle this all more cleanly
79
+ @html_text = false
80
+
81
+ if view_file
82
+ return if is_erb_txt(line)
83
+ return if is_javascript(line)
84
+
85
+ if m = find_printed_erb(line)
86
+ out = parse_for_strings(m[0])
87
+ else
88
+ result = Nokogiri::HTML(line)
89
+ @html_text = true
90
+ out = result.text().empty? ? out : result.text()
91
+ end
92
+ elsif result = inline_strings(line)
93
+ out = result
94
+ else
95
+ out = parse_for_strings(line)
96
+ end
97
+
98
+ @content_arry = chew(out)
99
+ end
100
+
101
+
102
+
103
+ #
104
+ # Get rid of strings that are only whitespace, only digits, or are variable names
105
+ # e.g. wombat_love_id, 55, ''
106
+ #
107
+ def chew(prsd_arry)
108
+ prsd_arry.select do |parsed_string|
109
+ parsed_string.match(/[\S]/) &&
110
+ parsed_string.match(/[\D]/) &&
111
+ !parsed_string.match(/txt\.[\w]*\.[\w]*/) &&
112
+ !(parsed_string.match(/[\s]/).nil? && parsed_string.match(/[_-]/))
113
+ end
114
+ end
115
+
116
+
117
+
118
+ #
119
+ # Take each piece of found content, search
120
+ # it for embedded variables, construct a new key
121
+ # for the content line and an i18n call for the new content.
122
+ # Key's mainword is longest word of the string
123
+ #
124
+ # Returns:
125
+ # localized_string = I18n.t('txt.admin.file_path.success', :organization => organization)
126
+ # key = txt.admin.file_path.success
127
+ #
128
+ def digest(content)
129
+ vars = find_variables(content)
130
+
131
+ cur_path = @file.path.split('/',2).last
132
+ cur_path = cur_path.split('.').first
133
+ cur_path.gsub!('/','.')
134
+
135
+ words = content.scan(/\w+/)
136
+ identifier = words[0,5].join('_')
137
+
138
+ key_name = "txt.admin." + cur_path + '.' + identifier
139
+ localized_string = "I18n.t('#{key_name}'"
140
+
141
+ if vars
142
+ vars.each { |v| localized_string << ", :#{v} => #{v}" }
143
+ end
144
+ localized_string << ")"
145
+
146
+ return localized_string, key_name
147
+ end
148
+
149
+
150
+ #
151
+ # If content is present, generate 18n for it and add it to tmp
152
+ # source file and yml file.
153
+ # Othewise pass through original txt
154
+ # to tmp file.
155
+ #
156
+ def speak(line)
157
+ return unless @command_speak
158
+
159
+ f_name = File.basename(@file.path)
160
+ @tmp_file ||= Tempfile.new(f_name)
161
+ @yml_file ||= File.open(self.class.default_yml, "a+")
162
+
163
+
164
+ if !@content_arry.empty?
165
+ replacement_arry=[]
166
+ @content_arry.each do |content|
167
+ i18n_string, key_name = digest(content)
168
+ replacement_arry << [i18n_string, content, key_name]
169
+ end
170
+
171
+ speak_source_file(line, replacement_arry)
172
+ if !@interactive || @localize_now
173
+ speak_yml(replacement_arry)
174
+ end
175
+ else
176
+ @tmp_file.write(line)
177
+ end
178
+ end
179
+
180
+
181
+
182
+ #
183
+ # Construct a diff like format in tmp file
184
+ # for i18n string
185
+ #
186
+ def speak_source_file(line, replacement_arry)
187
+
188
+ localized_line = line.dup
189
+ replacement_arry.each do |i18n_string, content|
190
+ replacement_string = @html_text ? "<%= "+ i18n_string + " %>" : i18n_string
191
+ localized_line.gsub!(content, replacement_string)
192
+ end
193
+
194
+ if @interactive
195
+ write_diffs(STDOUT, line, localized_line)
196
+ speak_to_me
197
+ @localize_now ? @tmp_file.write(localized_line) : @tmp_file.write(line)
198
+ else
199
+ write_diffs(@tmp_file, line, localized_line)
200
+ end
201
+ end
202
+
203
+
204
+ #
205
+ # Ask whether to localize the string now or not
206
+ #
207
+ def speak_to_me
208
+ begin
209
+ puts "Localize string now? (y/n)"
210
+ answer = STDIN.gets
211
+ end while !answer.match(/^(y|n)/)
212
+ @localize_now = answer.include?("y")? true : false
213
+ end
214
+
215
+
216
+ def write_diffs(output_via, line, localized_line)
217
+ output_via.write("<<<<<<<<<<\n")
218
+ output_via.write("#{localized_line}\n")
219
+ output_via.write("==========\n")
220
+ output_via.write(line)
221
+ output_via.write(">>>>>>>>>>\n")
222
+ end
223
+
224
+ #
225
+ # Add translation key to yml file
226
+ #
227
+ def speak_yml(replacement_arry)
228
+ replacement_arry.each do |i8n_string, content, key_name|
229
+ quoteless_content = content.gsub(/["']/,'')
230
+ yml_string = "\n - translation:\n"
231
+ yml_string << " key: \"#{key_name}\"\n"
232
+ yml_string << " title: \"#{quoteless_content} label\"\n"
233
+ yml_string << " value: \"#{quoteless_content}\"\n"
234
+
235
+ @yml_file.write("\n<<<<<<<<<<\n") unless @localize_now
236
+ @yml_file.write(yml_string)
237
+ @yml_file.write(">>>>>>>>>>\n") unless @localize_now
238
+ end
239
+ end
240
+
241
+
242
+ #
243
+ # Print matches to STDOUT
244
+ #
245
+ def howl
246
+ @prize.each { |p| puts "#{p[:filename]} : #{p[:line_number]}\t\t #{p[:value]}" }
247
+ end
248
+
249
+
250
+ #
251
+ # Close all files and rename tmp file to real source file
252
+ #
253
+ def file_cleanup
254
+ if @tmp_file
255
+ @tmp_file.close
256
+ FileUtils.mv(@tmp_file.path, @file.path)
257
+ @tmp_file = nil
258
+ end
259
+ end
260
+
261
+ end
@@ -0,0 +1,42 @@
1
+ require 'string_hound'
2
+
3
+ desc "Given a directory, traverse through it and output all strings in all files to STDOUT."
4
+ namespace :hound do
5
+ task :hunt do
6
+ if ARGV.count < 2
7
+ puts "Incorrect number of arguments. Please give a directory name"
8
+ return
9
+ end
10
+ dir = ARGV.pop
11
+ hound = StringHound.new(dir)
12
+ hound.hunt
13
+ hound.howl
14
+ end
15
+
16
+ desc "Same as hunt, except instead of outputting to STDOUT, 'speak' inserts i18 strings into source files and adds keys to default yml file"
17
+ task :speak do
18
+ if ARGV.count < 2
19
+ puts "Incorrect number of arguments. Please give a directory name"
20
+ return
21
+ end
22
+
23
+ dir = ARGV.pop
24
+ hound = StringHound.new(dir)
25
+ hound.command_speak = true
26
+ hound.hunt
27
+ end
28
+
29
+ desc "Same as speak, except instead of inserting diffs directly into file, it asks permission to accept or deny the generated new string"
30
+ task :play do
31
+ if ARGV.count < 2
32
+ puts "Incorrect number of arguments. Please give a directory name"
33
+ return
34
+ end
35
+
36
+ dir = ARGV.pop
37
+ hound = StringHound.new(dir,{:interactive => true})
38
+ hound.command_speak = true
39
+ hound.hunt
40
+ end
41
+
42
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: string_hound
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Noel Dellofano
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-09-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Bark! hunts for strings.
14
+ email: noel@zendesk.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/regex_utils.rb
20
+ - lib/string_hound.rb
21
+ - lib/string_hound/tasks.rb
22
+ homepage: https://github.com/pinkvelociraptor/string_hound
23
+ licenses: []
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.0.6
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: string_hound
45
+ test_files: []