string_hound 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []