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.
- 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: []
|