ruby-zoom 4.7.5 → 5.0.0
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 +4 -4
- data/bin/z +108 -50
- data/bin/zc +108 -50
- data/bin/zf +108 -50
- data/bin/zg +108 -50
- data/bin/zl +108 -50
- data/bin/zr +108 -50
- data/lib/zoom.rb +89 -26
- data/lib/zoom/cache.rb +10 -9
- data/lib/zoom/cache/result.rb +3 -3
- data/lib/zoom/config.rb +15 -6
- data/lib/zoom/editor.rb +8 -4
- data/lib/zoom/error.rb +1 -0
- data/lib/zoom/error/regex_not_provided.rb +5 -0
- data/lib/zoom/profile.rb +62 -90
- data/lib/zoom/profile/ack.rb +35 -10
- data/lib/zoom/profile/ag.rb +21 -5
- data/lib/zoom/profile/find.rb +48 -4
- data/lib/zoom/profile/grep.rb +25 -5
- data/lib/zoom/profile/passwords.rb +5 -13
- data/lib/zoom/profile/pt.rb +21 -5
- data/lib/zoom/profile/rg.rb +51 -0
- data/lib/zoom/profile/unsafe_c.rb +32 -30
- data/lib/zoom/profile/unsafe_java.rb +15 -28
- data/lib/zoom/profile/unsafe_js.rb +5 -12
- data/lib/zoom/profile/unsafe_php.rb +48 -55
- data/lib/zoom/profile/unsafe_python.rb +16 -24
- data/lib/zoom/profile/unsafe_ruby.rb +16 -25
- data/lib/zoom/profile_manager.rb +50 -16
- data/lib/zoom/security_profile.rb +60 -5
- data/lib/zoom/wish/edit_wish.rb +4 -4
- data/lib/zoom/wish/editor_wish.rb +2 -8
- metadata +14 -12
data/lib/zoom.rb
CHANGED
@@ -5,6 +5,77 @@ class Zoom
|
|
5
5
|
attr_reader :cache
|
6
6
|
attr_reader :config
|
7
7
|
|
8
|
+
def ensure_valid_header(header)
|
9
|
+
# Ensure header has no nil
|
10
|
+
header["args"] ||= ""
|
11
|
+
header["debug"] ||= false
|
12
|
+
header["paths"] ||= "."
|
13
|
+
header["paths"] = "." if (header["paths"].empty?)
|
14
|
+
header["profile_name"] ||= ""
|
15
|
+
header["pwd"] = Dir.pwd
|
16
|
+
header["regex"] ||= ""
|
17
|
+
header["translate"] ||= Hash.new
|
18
|
+
|
19
|
+
# If no profile name, use the current profile
|
20
|
+
profile_name = header["profile_name"]
|
21
|
+
if (profile_name.empty?)
|
22
|
+
profile_name = @config.current_profile_name
|
23
|
+
header["profile_name"] = profile_name
|
24
|
+
end
|
25
|
+
|
26
|
+
if (!@config.has_profile?(profile_name))
|
27
|
+
raise Zoom::Error::ProfileDoesNotExist.new(profile_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
profile = @config.get_profile(profile_name)
|
31
|
+
|
32
|
+
# Use hard-coded regex if defined
|
33
|
+
if (
|
34
|
+
profile.regex &&
|
35
|
+
!profile.regex.empty? &&
|
36
|
+
(header["regex"] != profile.regex)
|
37
|
+
)
|
38
|
+
# If there was a regex then it may be an arg or a path
|
39
|
+
if (!header["regex"].empty?)
|
40
|
+
if (Pathname.new(header["regex"]).exist?)
|
41
|
+
header["paths"] = "" if (header["paths"] == ".")
|
42
|
+
paths = header["paths"].split(" ")
|
43
|
+
paths.insert(0, header["regex"])
|
44
|
+
header["paths"] = paths.join(" ")
|
45
|
+
else
|
46
|
+
header["args"] += " #{header["regex"]}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
header["regex"] = profile.regex
|
51
|
+
end
|
52
|
+
|
53
|
+
# If using a search tool
|
54
|
+
if (!profile.format_flags.empty?)
|
55
|
+
if (header["regex"].empty? && header["paths"] == ".")
|
56
|
+
# Throw exception because no regex was provided or
|
57
|
+
# hard-coded
|
58
|
+
raise Zoom::Error::RegexNotProvided.new
|
59
|
+
end
|
60
|
+
|
61
|
+
# This isn't done here anymore as it breaks stuff
|
62
|
+
# header["regex"] = header["regex"].shellescape
|
63
|
+
end
|
64
|
+
|
65
|
+
# Strip values
|
66
|
+
header.keys.each do |key|
|
67
|
+
next if (key == "regex")
|
68
|
+
begin
|
69
|
+
header[key].strip!
|
70
|
+
rescue
|
71
|
+
# Wasn't a String
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
return header
|
76
|
+
end
|
77
|
+
private :ensure_valid_header
|
78
|
+
|
8
79
|
def self.hilight(hilight = true)
|
9
80
|
@@hilight = hilight
|
10
81
|
end
|
@@ -22,10 +93,7 @@ class Zoom
|
|
22
93
|
|
23
94
|
def open(results)
|
24
95
|
return if (results.nil? || results.empty?)
|
25
|
-
|
26
|
-
# All results should be from the same profile
|
27
|
-
profile = @config.get_profile(results[0].profile_name)
|
28
|
-
profile.go(@config.editor, results)
|
96
|
+
Zoom::Editor.new(@config.editor).open(results)
|
29
97
|
end
|
30
98
|
|
31
99
|
def repeat(shortcut = true)
|
@@ -33,39 +101,34 @@ class Zoom
|
|
33
101
|
run(@cache.header, shortcut, true)
|
34
102
|
end
|
35
103
|
|
36
|
-
def run(
|
37
|
-
#
|
38
|
-
|
39
|
-
header[key] ||= ""
|
40
|
-
end
|
41
|
-
header["paths"] ||= "."
|
42
|
-
header["pwd"] = Dir.pwd
|
43
|
-
header["translate"] ||= Array.new
|
44
|
-
|
45
|
-
profile_name = header["profile_name"]
|
46
|
-
if (profile_name.empty?)
|
47
|
-
profile_name = @config.current_profile_name
|
48
|
-
header["profile_name"] = profile_name
|
49
|
-
end
|
104
|
+
def run(h, shortcut = true, repeat = false)
|
105
|
+
# Don't change the header passed in
|
106
|
+
header = h.clone
|
50
107
|
|
51
|
-
|
52
|
-
|
53
|
-
end
|
108
|
+
# Ensure header is formatted properly and valid
|
109
|
+
header = ensure_valid_header(header) if (!repeat)
|
54
110
|
|
111
|
+
profile_name = header["profile_name"]
|
55
112
|
profile = @config.get_profile(profile_name)
|
56
|
-
begin
|
57
|
-
# This will translate and/or append args such that the
|
58
|
-
# output will be something Zoom can process
|
59
|
-
header = profile.preprocess(header) if (!repeat)
|
60
113
|
|
114
|
+
begin
|
61
115
|
# Clear cache
|
62
116
|
@cache.clear
|
63
117
|
|
64
118
|
# Store needed details
|
65
119
|
@cache.header(header)
|
66
120
|
|
121
|
+
# Translate any needed flags
|
122
|
+
header["translated"] = profile.translate(
|
123
|
+
header["translate"]
|
124
|
+
).strip
|
125
|
+
|
126
|
+
# This may append args such that the output will be
|
127
|
+
# something Zoom can process
|
128
|
+
header = profile.preprocess(header)
|
129
|
+
|
67
130
|
# Execute profile
|
68
|
-
@cache.write(profile.exe(header), header["
|
131
|
+
@cache.write(profile.exe(header), header["regex"])
|
69
132
|
|
70
133
|
# Display results from cache
|
71
134
|
@cache.shortcut(@config) if (shortcut)
|
data/lib/zoom/cache.rb
CHANGED
@@ -111,11 +111,6 @@ class Zoom::Cache
|
|
111
111
|
return @header["paths"]
|
112
112
|
end
|
113
113
|
|
114
|
-
def pattern
|
115
|
-
return nil if (@header.nil?)
|
116
|
-
return @header["pattern"]
|
117
|
-
end
|
118
|
-
|
119
114
|
def profile_name
|
120
115
|
return nil if (@header.nil?)
|
121
116
|
return @header["profile_name"]
|
@@ -155,6 +150,11 @@ class Zoom::Cache
|
|
155
150
|
end
|
156
151
|
end
|
157
152
|
|
153
|
+
def regex
|
154
|
+
return nil if (@header.nil?)
|
155
|
+
return @header["regex"]
|
156
|
+
end
|
157
|
+
|
158
158
|
def shortcut(config)
|
159
159
|
return if (empty?)
|
160
160
|
|
@@ -184,7 +184,7 @@ class Zoom::Cache
|
|
184
184
|
config.hilight_tag("[#{result.tag}]"),
|
185
185
|
"#{config.hilight_lineno(result.lineno)}:",
|
186
186
|
result.match.gsub(
|
187
|
-
/(#{
|
187
|
+
/(#{regex})/i,
|
188
188
|
config.hilight_match("\\1")
|
189
189
|
)
|
190
190
|
].join(" ")
|
@@ -204,7 +204,7 @@ class Zoom::Cache
|
|
204
204
|
end
|
205
205
|
end
|
206
206
|
|
207
|
-
def write(str,
|
207
|
+
def write(str, r = nil)
|
208
208
|
return if (str.nil?)
|
209
209
|
|
210
210
|
if (!str.valid_encoding?)
|
@@ -217,15 +217,16 @@ class Zoom::Cache
|
|
217
217
|
|
218
218
|
File.open(@cache_file, "a") do |f|
|
219
219
|
str.gsub(/\r/, "^M").split("\n").each do |line|
|
220
|
-
if (
|
220
|
+
if (r && line.match(/^[^:]+:[0-9]+:.+$/))
|
221
221
|
line.match(/^([^:]+:[0-9]+:)(.+)$/) do |m|
|
222
222
|
m[2].scan(
|
223
|
-
/(.{0,128}#{
|
223
|
+
/(.{0,128}#{r}.{0,128})/i
|
224
224
|
) do |n|
|
225
225
|
f.write("#{m[1]}#{n[0]}\n")
|
226
226
|
end
|
227
227
|
end
|
228
228
|
else
|
229
|
+
line.gsub!(/^(.{128}).*(.{128})$/, "\\1...\\2")
|
229
230
|
f.write("#{line}\n")
|
230
231
|
end
|
231
232
|
end
|
data/lib/zoom/cache/result.rb
CHANGED
@@ -28,12 +28,12 @@ class Zoom::Cache::Result
|
|
28
28
|
@filename = "Binary file"
|
29
29
|
@contents = m[1]
|
30
30
|
end
|
31
|
-
when /^([^:]+)
|
32
|
-
@contents.match(/^([^:]+)
|
31
|
+
when /^([^:]+)[:-](\d+)[:-](.*)$/
|
32
|
+
@contents.match(/^([^:]+)[:-](\d+)[:-](.*)$/) do |m|
|
33
33
|
next if (m.nil?)
|
34
34
|
|
35
35
|
@grep_like = true
|
36
|
-
@filename = m[1]
|
36
|
+
@filename = m[1].gsub(/^\.\//, "")
|
37
37
|
@lineno = m[2]
|
38
38
|
@match = m[3]
|
39
39
|
end
|
data/lib/zoom/config.rb
CHANGED
@@ -49,7 +49,7 @@ class Zoom::Config < JSONConfig
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def default_config
|
52
|
-
default = Zoom::ProfileManager.
|
52
|
+
default = Zoom::ProfileManager.default_tool
|
53
53
|
profiles = Zoom::ProfileManager.default_profiles
|
54
54
|
|
55
55
|
clear
|
@@ -64,20 +64,29 @@ class Zoom::Config < JSONConfig
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def editor(ed = nil)
|
67
|
-
if (ed)
|
68
|
-
e =
|
69
|
-
|
67
|
+
if (ed && !ed.empty?)
|
68
|
+
e = ed.split(" ")[0]
|
69
|
+
if (ScoobyDoo.where_are_you(e).nil?)
|
70
|
+
raise Zoom::Error::ExecutableNotFound.new(e)
|
71
|
+
end
|
70
72
|
set("editor", ed)
|
71
73
|
end
|
72
74
|
|
73
75
|
e = get("editor")
|
74
76
|
e = ENV["EDITOR"] if (e.nil? || e.empty?)
|
75
77
|
e = "vim" if (e.nil? || e.empty?)
|
78
|
+
|
79
|
+
e, _, f = e.partition(" ")
|
76
80
|
e = ScoobyDoo.where_are_you(e)
|
77
|
-
|
81
|
+
|
82
|
+
if (e.nil?)
|
83
|
+
e = ScoobyDoo.where_are_you("vi")
|
84
|
+
f = ""
|
85
|
+
end
|
86
|
+
|
78
87
|
raise Zoom::Error::ExecutableNotFound.new("vi") if (e.nil?)
|
79
88
|
|
80
|
-
return e
|
89
|
+
return "#{e} #{f}".strip
|
81
90
|
end
|
82
91
|
|
83
92
|
def has_profile?(name)
|
data/lib/zoom/editor.rb
CHANGED
@@ -42,7 +42,7 @@ class Zoom::Editor
|
|
42
42
|
private :default
|
43
43
|
|
44
44
|
def initialize(editor)
|
45
|
-
@editor = editor
|
45
|
+
@editor, _, @flags = editor.partition(" ")
|
46
46
|
end
|
47
47
|
|
48
48
|
def open(results)
|
@@ -61,11 +61,13 @@ class Zoom::Editor
|
|
61
61
|
filename = result.filename
|
62
62
|
lineno = result.lineno
|
63
63
|
pwd = result.pwd
|
64
|
-
system(
|
64
|
+
system(
|
65
|
+
"#{@editor} #{@flags} +#{lineno} '#{pwd}/#{filename}'"
|
66
|
+
)
|
65
67
|
else
|
66
68
|
filename = result.contents
|
67
69
|
pwd = result.pwd
|
68
|
-
system("#{@editor} '#{pwd}/#{filename}'")
|
70
|
+
system("#{@editor} #{@flags} '#{pwd}/#{filename}'")
|
69
71
|
end
|
70
72
|
end
|
71
73
|
private :open_result
|
@@ -113,7 +115,9 @@ class Zoom::Editor
|
|
113
115
|
zq.close
|
114
116
|
zs.close
|
115
117
|
|
116
|
-
system(
|
118
|
+
system(
|
119
|
+
"#{@editor} #{@flags} -S #{source} '#{files.join("' '")}'"
|
120
|
+
)
|
117
121
|
|
118
122
|
FileUtils.rm_f(quickfix)
|
119
123
|
FileUtils.rm_f(source)
|
data/lib/zoom/error.rb
CHANGED
data/lib/zoom/profile.rb
CHANGED
@@ -3,9 +3,11 @@ require "scoobydoo"
|
|
3
3
|
require "shellwords"
|
4
4
|
|
5
5
|
class Zoom::Profile < Hash
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
attr_accessor :exts
|
7
|
+
attr_accessor :files
|
8
|
+
attr_accessor :format_flags
|
9
|
+
attr_accessor :regex
|
10
|
+
attr_accessor :taggable
|
9
11
|
|
10
12
|
def after(a = nil)
|
11
13
|
self["after"] = a.strip if (a)
|
@@ -35,30 +37,26 @@ class Zoom::Profile < Hash
|
|
35
37
|
|
36
38
|
def exe(header)
|
37
39
|
# Emulate grep
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
40
|
+
cmd = [
|
41
|
+
before,
|
42
|
+
tool,
|
43
|
+
@format_flags,
|
44
|
+
flags,
|
45
|
+
only_exts_and_files,
|
46
|
+
header["translated"],
|
47
|
+
header["args"],
|
48
|
+
"--",
|
49
|
+
header["regex"].shellescape,
|
50
|
+
header["paths"],
|
51
|
+
after
|
52
|
+
].join(" ").strip
|
53
|
+
|
54
|
+
if (header.has_key?("debug") && header["debug"])
|
55
|
+
puts cmd
|
56
|
+
return ""
|
49
57
|
else
|
50
|
-
cmd
|
51
|
-
before,
|
52
|
-
operator,
|
53
|
-
@format_flags,
|
54
|
-
flags,
|
55
|
-
header["args"],
|
56
|
-
header["pattern"].shellescape,
|
57
|
-
header["paths"],
|
58
|
-
after
|
59
|
-
].join(" ").strip
|
58
|
+
return %x(#{cmd})
|
60
59
|
end
|
61
|
-
return %x(#{cmd})
|
62
60
|
end
|
63
61
|
|
64
62
|
def flags(f = nil)
|
@@ -71,7 +69,7 @@ class Zoom::Profile < Hash
|
|
71
69
|
begin
|
72
70
|
return profile_by_name(json["class"]).new(
|
73
71
|
json["name"],
|
74
|
-
json["
|
72
|
+
json["tool"].nil? ? "" : json["tool"],
|
75
73
|
json["flags"].nil? ? "" : json["flags"],
|
76
74
|
json["before"].nil? ? "" : json["before"],
|
77
75
|
json["after"].nil? ? "" : json["after"]
|
@@ -83,8 +81,9 @@ class Zoom::Profile < Hash
|
|
83
81
|
end
|
84
82
|
end
|
85
83
|
|
86
|
-
def
|
87
|
-
|
84
|
+
def grep_like_format_flags(all = false)
|
85
|
+
@format_flags = "" # Set this to mirror basic grep
|
86
|
+
@taggable = false # Should results be tagged like grep
|
88
87
|
end
|
89
88
|
|
90
89
|
def hilight_after(str)
|
@@ -117,34 +116,41 @@ class Zoom::Profile < Hash
|
|
117
116
|
end
|
118
117
|
private :hilight_name
|
119
118
|
|
120
|
-
def
|
119
|
+
def hilight_regex(str)
|
121
120
|
return str if (!Zoom.hilight?)
|
122
|
-
return str
|
121
|
+
return str
|
123
122
|
end
|
124
|
-
private :
|
123
|
+
private :hilight_regex
|
125
124
|
|
126
|
-
def
|
125
|
+
def hilight_tool(str)
|
127
126
|
return str if (!Zoom.hilight?)
|
128
|
-
return str
|
127
|
+
return str.green
|
129
128
|
end
|
130
|
-
private :
|
129
|
+
private :hilight_tool
|
131
130
|
|
132
|
-
def initialize(n = nil,
|
131
|
+
def initialize(n = nil, t = nil, f = nil, b = nil, a = nil)
|
133
132
|
a ||= ""
|
134
133
|
b ||= ""
|
135
134
|
f ||= ""
|
136
135
|
n ||= camel_case_to_underscore(self.class.to_s)
|
137
|
-
|
136
|
+
t ||= "echo"
|
138
137
|
|
139
138
|
self["class"] = self.class.to_s
|
140
139
|
after(a)
|
141
140
|
before(b)
|
142
141
|
flags(f)
|
143
142
|
name(n)
|
144
|
-
|
143
|
+
tool(t)
|
144
|
+
|
145
|
+
@exts = Array.new # Set this to only search specified exts
|
146
|
+
@files = Array.new # Set this to noly search specified files
|
147
|
+
@regex = "" # Setting this will override user input
|
145
148
|
|
146
|
-
|
149
|
+
# In case someone overrides grep_like_format_flags
|
150
|
+
@format_flags = ""
|
147
151
|
@taggable = false
|
152
|
+
|
153
|
+
grep_like_format_flags
|
148
154
|
end
|
149
155
|
|
150
156
|
def name(n = nil)
|
@@ -153,56 +159,12 @@ class Zoom::Profile < Hash
|
|
153
159
|
return self["name"]
|
154
160
|
end
|
155
161
|
|
156
|
-
def
|
157
|
-
|
158
|
-
|
159
|
-
op = ScoobyDoo.where_are_you(o)
|
160
|
-
raise Zoom::Error::ExecutableNotFound.new(o) if (op.nil?)
|
161
|
-
self["operator"] = o
|
162
|
-
end
|
163
|
-
return self["operator"]
|
162
|
+
def only_exts_and_files
|
163
|
+
# Do nothing
|
164
|
+
return ""
|
164
165
|
end
|
165
166
|
|
166
167
|
def preprocess(header)
|
167
|
-
# Use hard-coded pattern if defined
|
168
|
-
if (
|
169
|
-
@pattern &&
|
170
|
-
!@pattern.empty? &&
|
171
|
-
(header["pattern"] != @pattern)
|
172
|
-
)
|
173
|
-
header["args"] += " #{header["pattern"]}"
|
174
|
-
header["pattern"] = @pattern
|
175
|
-
end
|
176
|
-
|
177
|
-
case operator.split("/")[-1]
|
178
|
-
when /^ack(-grep)?$/, "ag", "grep", "pt"
|
179
|
-
paths = header["paths"].split(" ")
|
180
|
-
if (header["pattern"].empty? && !paths.empty?)
|
181
|
-
header["pattern"] = paths.delete_at(0)
|
182
|
-
header["paths"] = paths.join(" ").strip
|
183
|
-
header["paths"] = "." if (header["paths"].empty?)
|
184
|
-
end
|
185
|
-
|
186
|
-
# This isn't done here anymore as it'll break hilighting
|
187
|
-
# header["pattern"] = header["pattern"].shellescape
|
188
|
-
when "find"
|
189
|
-
# If additional args are passed, then assume pattern is
|
190
|
-
# actually an arg
|
191
|
-
if (header["args"] && !header["args"].empty?)
|
192
|
-
header["args"] += " #{header["pattern"]}"
|
193
|
-
header["pattern"] = ""
|
194
|
-
end
|
195
|
-
|
196
|
-
# If pattern was provided then assume it's an iname search
|
197
|
-
if (header["pattern"] && !header["pattern"].empty?)
|
198
|
-
header["pattern"] = "-iname \"#{header["pattern"]}\""
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
# Translate any needed flags
|
203
|
-
header["args"] += " #{translate(header["translate"])}"
|
204
|
-
header["args"].strip!
|
205
|
-
|
206
168
|
return header
|
207
169
|
end
|
208
170
|
|
@@ -216,7 +178,7 @@ class Zoom::Profile < Hash
|
|
216
178
|
ObjectSpace.each_object(Class).select do |clas|
|
217
179
|
if (clas < self)
|
218
180
|
begin
|
219
|
-
clas.new
|
181
|
+
clas.new
|
220
182
|
true
|
221
183
|
rescue Zoom::Error::ExecutableNotFound
|
222
184
|
false
|
@@ -232,17 +194,27 @@ class Zoom::Profile < Hash
|
|
232
194
|
ret.push(hilight_name)
|
233
195
|
ret.push("#{hilight_class}\n")
|
234
196
|
ret.push(hilight_before(before)) if (!before.empty?)
|
235
|
-
ret.push(
|
197
|
+
ret.push(hilight_tool(tool)) if (!tool.empty?)
|
236
198
|
ret.push(hilight_flags(flags)) if (!flags.empty?)
|
237
|
-
if (@
|
238
|
-
ret.push(
|
199
|
+
if (@regex.nil? || @regex.empty?)
|
200
|
+
ret.push(hilight_regex("REGEX"))
|
239
201
|
else
|
240
|
-
ret.push(
|
202
|
+
ret.push(hilight_regex("\"#{@regex}\""))
|
241
203
|
end
|
242
204
|
ret.push(hilight_after(after)) if (!after.empty?)
|
243
205
|
return ret.join(" ").strip
|
244
206
|
end
|
245
207
|
|
208
|
+
def tool(t = nil)
|
209
|
+
if (t)
|
210
|
+
t.strip!
|
211
|
+
tl = ScoobyDoo.where_are_you(t)
|
212
|
+
raise Zoom::Error::ExecutableNotFound.new(t) if (tl.nil?)
|
213
|
+
self["tool"] = t
|
214
|
+
end
|
215
|
+
return self["tool"]
|
216
|
+
end
|
217
|
+
|
246
218
|
def translate(from)
|
247
219
|
return ""
|
248
220
|
end
|