string_hound 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/regex_utils.rb +37 -0
- data/lib/string_hound.rb +261 -0
- data/lib/string_hound/tasks.rb +42 -0
- metadata +45 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/regex_utils.rb
ADDED
@@ -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
|
data/lib/string_hound.rb
ADDED
@@ -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: []
|