humanized 0.0.1.alpha
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.
- data/lib/humanized.rb +52 -0
- data/lib/humanized/compiler.rb +127 -0
- data/lib/humanized/core_ext/array.rb +25 -0
- data/lib/humanized/core_ext/date.rb +21 -0
- data/lib/humanized/core_ext/hash.rb +25 -0
- data/lib/humanized/core_ext/module.rb +47 -0
- data/lib/humanized/core_ext/nilclass.rb +21 -0
- data/lib/humanized/core_ext/numeric.rb +21 -0
- data/lib/humanized/core_ext/object.rb +24 -0
- data/lib/humanized/core_ext/string.rb +21 -0
- data/lib/humanized/core_ext/symbol.rb +21 -0
- data/lib/humanized/core_ext/time.rb +21 -0
- data/lib/humanized/humanizer.rb +122 -0
- data/lib/humanized/interpolation/conjunctions.rb +34 -0
- data/lib/humanized/interpolation/date.rb +36 -0
- data/lib/humanized/interpolation/english.rb +83 -0
- data/lib/humanized/interpolation/german.rb +74 -0
- data/lib/humanized/interpolation/kng.rb +217 -0
- data/lib/humanized/interpolation/number.rb +36 -0
- data/lib/humanized/ref.rb +29 -0
- data/lib/humanized/scope.rb +264 -0
- data/lib/humanized/source.rb +169 -0
- data/lib/humanized/wrapper.rb +74 -0
- data/lib/more/humanized/json_source.rb +50 -0
- data/lib/more/humanized/yaml_source.rb +28 -0
- metadata +149 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# This program is free software: you can redistribute it and/or modify
|
3
|
+
# it under the terms of the Affero GNU General Public License as published by
|
4
|
+
# the Free Software Foundation, either version 3 of the License, or
|
5
|
+
# (at your option) any later version.
|
6
|
+
#
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
10
|
+
# GNU General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License
|
13
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
|
+
#
|
15
|
+
# (c) 2011 by Hannes Georg
|
16
|
+
#
|
17
|
+
|
18
|
+
module Humanized
|
19
|
+
module Number
|
20
|
+
|
21
|
+
def number(humanizer, number, format = 'default')
|
22
|
+
if format == 'default'
|
23
|
+
it = number._(:format,:default)
|
24
|
+
else
|
25
|
+
it = number._.format( format._ | :default._ )
|
26
|
+
end
|
27
|
+
f = humanizer.get(it)
|
28
|
+
if f.kind_of? String
|
29
|
+
return sprintf(f,number)
|
30
|
+
end
|
31
|
+
warn "Unable to find Number format: #{it.inspect}."
|
32
|
+
return ''
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# This program is free software: you can redistribute it and/or modify
|
3
|
+
# it under the terms of the Affero GNU General Public License as published by
|
4
|
+
# the Free Software Foundation, either version 3 of the License, or
|
5
|
+
# (at your option) any later version.
|
6
|
+
#
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
10
|
+
# GNU General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License
|
13
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
|
+
#
|
15
|
+
# (c) 2011 by Hannes Georg
|
16
|
+
#
|
17
|
+
|
18
|
+
module Humanized
|
19
|
+
|
20
|
+
# A Reference can be used to redirect lookups for certain paths.
|
21
|
+
class Ref < Array
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
'!ref'+super
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,264 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# This program is free software: you can redistribute it and/or modify
|
3
|
+
# it under the terms of the Affero GNU General Public License as published by
|
4
|
+
# the Free Software Foundation, either version 3 of the License, or
|
5
|
+
# (at your option) any later version.
|
6
|
+
#
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
10
|
+
# GNU General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License
|
13
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
|
+
#
|
15
|
+
# (c) 2011 by Hannes Georg
|
16
|
+
#
|
17
|
+
|
18
|
+
module Humanized
|
19
|
+
# A {Scope} is _the_ way to tell a {Humanizer} what you want from it.
|
20
|
+
# It contains of three parts:
|
21
|
+
# * {#path a list of paths}, which will be looked up in a {Source source}
|
22
|
+
# * {#default a default}, which will be used if nothing was found
|
23
|
+
# * {#variables variables}, which will be used to interpolate a found string
|
24
|
+
# That's all you need!
|
25
|
+
# The good thing: you'll unlikly create a scope by hand, that's done automatically with "_"!
|
26
|
+
#
|
27
|
+
# == Examples
|
28
|
+
#
|
29
|
+
# The basic steps:
|
30
|
+
# # Creates a scope which looks up ":a" with no default and no variables:
|
31
|
+
# :a._
|
32
|
+
# # Creates a scope which looks up nothing, has a default of "String" but no variables:
|
33
|
+
# "String"._
|
34
|
+
# # Creates a scope which looks up nothing, has no default but the variable :foo = "bar"
|
35
|
+
# {:foo => 'bar'}._
|
36
|
+
#
|
37
|
+
# Combining these steps brings the power:
|
38
|
+
#
|
39
|
+
# # Creates a scope which looks up ":a", has a default of "String" and the variable :foo = "bar"
|
40
|
+
# :a._ + "String"._ + {:foo => 'bar'}._
|
41
|
+
# # Shorthand for this:
|
42
|
+
# :a._("String", :foo => 'bar')
|
43
|
+
#
|
44
|
+
# The "_"-method is overloaded for many things. For example for inheritance:
|
45
|
+
#
|
46
|
+
# module Site
|
47
|
+
# class User
|
48
|
+
# end
|
49
|
+
# class Admin < User
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
# # Creates a scope matching ":site, :admin" or ":site, :user":
|
53
|
+
# Site::Admin._
|
54
|
+
# # This creates the same:
|
55
|
+
# Site::Admin.new._
|
56
|
+
#
|
57
|
+
# And for Arrays:
|
58
|
+
# # This matches ":a, :b, :c":
|
59
|
+
# [:a, :b, :c]._
|
60
|
+
#
|
61
|
+
# Finally for Scopes itself:
|
62
|
+
# # Given scope is a Scope this is always true:
|
63
|
+
# scope._._ == scope._
|
64
|
+
#
|
65
|
+
# I could continue the whole day ...
|
66
|
+
#
|
67
|
+
# == Tricks
|
68
|
+
# A Scope responds to any method giving a new Scope suffixed by the method name
|
69
|
+
# # Looks up ":a, :b, :c"
|
70
|
+
# :a._.b.c
|
71
|
+
# "_" can also take a block which is instance evaled on the scope:
|
72
|
+
# # Looks up ":a, :b, :c"
|
73
|
+
# :a._{ b.c }
|
74
|
+
# # Looks up ":a, :x" or ":a, :y"
|
75
|
+
# :a._{ x | y }
|
76
|
+
# There are two special scopes:
|
77
|
+
# # Looks up "", which will we be the whole source
|
78
|
+
# Humanized::Scope::Root
|
79
|
+
# # Looks up nothing
|
80
|
+
# Humanized::Scope::None
|
81
|
+
#
|
82
|
+
class Scope
|
83
|
+
|
84
|
+
include Enumerable
|
85
|
+
# @private
|
86
|
+
UNMAGIC_METHODS = [:to_ary]
|
87
|
+
# @private
|
88
|
+
NAME_REGEX = /[a-z_]+/.freeze
|
89
|
+
# @private
|
90
|
+
OPTIONAL_NAME_REGEX = /([a-z_]+)\?/.freeze
|
91
|
+
|
92
|
+
attr_reader :path, :depth, :variables, :default
|
93
|
+
|
94
|
+
def self.from_str(str)
|
95
|
+
Scope.new([ str.explode('.').map(&:to_sym) ])
|
96
|
+
end
|
97
|
+
|
98
|
+
def initialize(path = [[]], depth = 1, variables = {}, default = nil)
|
99
|
+
@path = path.uniq
|
100
|
+
@path.each do |path|
|
101
|
+
path.freeze
|
102
|
+
end
|
103
|
+
@path.freeze
|
104
|
+
@depth = depth
|
105
|
+
@variables = variables
|
106
|
+
@default = default
|
107
|
+
end
|
108
|
+
|
109
|
+
# This method is a here to enable awesome DSL.
|
110
|
+
#== Example
|
111
|
+
# s = Scope.new
|
112
|
+
# s.defining.a.scope.using_methods # gives: (defining.a.scope.using_methods)
|
113
|
+
# s.defining(:a,:scope,:using_methods) # gives: (defining.a.scope.using_methods)
|
114
|
+
# s.this{ is.awesome | is.awful } # gives: (this.is.awesome , this.is.awful)
|
115
|
+
#
|
116
|
+
def method_missing(name, *args, &block)
|
117
|
+
return super if UNMAGIC_METHODS.include? name
|
118
|
+
name_str = name.to_s
|
119
|
+
if OPTIONAL_NAME_REGEX =~ name_str
|
120
|
+
return ( self + $1.to_sym | self )._(*args,&block)
|
121
|
+
end
|
122
|
+
if NAME_REGEX =~ name_str
|
123
|
+
return ( self + name )._(*args,&block)
|
124
|
+
end
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
def ==(other)
|
129
|
+
return false unless other.kind_of? Scope
|
130
|
+
return @path == other.path
|
131
|
+
end
|
132
|
+
|
133
|
+
# Creates a {Scope scope} which matches either self or the other scope.
|
134
|
+
# @example
|
135
|
+
# # this will match ":to_be" and ":not_to_be":
|
136
|
+
# ( :to_be._ | :not_to_be._ )
|
137
|
+
#
|
138
|
+
# @param [Scope] other another scope
|
139
|
+
# @return [Scope] a new scope
|
140
|
+
def |(other)
|
141
|
+
return other if @path.none?
|
142
|
+
return self.dup if other.none?
|
143
|
+
sp = self.path
|
144
|
+
sd = self.depth
|
145
|
+
op = other.path
|
146
|
+
od = other.depth
|
147
|
+
result = []
|
148
|
+
i = 0
|
149
|
+
j = 0
|
150
|
+
while i < sp.size and j < op.size
|
151
|
+
result.concat sp[i,sd] if sp.size > i
|
152
|
+
result.concat op[j,od] if op.size > j
|
153
|
+
i = i + sd
|
154
|
+
j = j + od
|
155
|
+
end
|
156
|
+
return Scope.new( result, sd + od , self.variables.merge(other.variables), other.default)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Creates a new scope which will optionally match this scope suffixed with the key.
|
160
|
+
#
|
161
|
+
# @example
|
162
|
+
# # this will match ":borat_is_stupid, :not" and ":borat_is_stupid":
|
163
|
+
# :borat_is_stupid._.optionally(:not)
|
164
|
+
#
|
165
|
+
# @param key
|
166
|
+
# @return [Scope] a new scope
|
167
|
+
def optionally(key)
|
168
|
+
return self._(key) | self
|
169
|
+
end
|
170
|
+
|
171
|
+
def [](*args)
|
172
|
+
sp = self.path
|
173
|
+
sd = self.depth
|
174
|
+
op = args
|
175
|
+
od = 1
|
176
|
+
result = []
|
177
|
+
self.each do |path|
|
178
|
+
args.each do |arg|
|
179
|
+
result << path + [arg]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
return Scope.new( result, args.size )
|
183
|
+
end
|
184
|
+
|
185
|
+
# Chain scopes together
|
186
|
+
# @example
|
187
|
+
# # this will match ":a,:b,:c"
|
188
|
+
# :a._ + :b._ + :c._
|
189
|
+
#
|
190
|
+
# @param *args an array of scopes for chaining
|
191
|
+
# @return [Scope]
|
192
|
+
def +(*args)
|
193
|
+
return self if args.none?
|
194
|
+
if( args.first.kind_of? Scope )
|
195
|
+
s = args.first
|
196
|
+
return Scope.new(@path, @depth, variables.merge(s.variables), self.default || s.default ) if @path.none? or s.path.none?
|
197
|
+
# TODO: maybe modify depth too?
|
198
|
+
new_path = []
|
199
|
+
@path.each do |x|
|
200
|
+
s.each do |path|
|
201
|
+
new_path << x + path
|
202
|
+
end
|
203
|
+
end
|
204
|
+
return Scope.new(new_path, s.depth, variables.merge(s.variables), self.default || s.default )
|
205
|
+
end
|
206
|
+
if @path.none?
|
207
|
+
return self
|
208
|
+
end
|
209
|
+
return Scope.new( @path.map{|x| x + args} , @depth , @variables, @default)
|
210
|
+
end
|
211
|
+
|
212
|
+
def _(*args,&block)
|
213
|
+
thiz = self
|
214
|
+
vars = nil
|
215
|
+
loop do
|
216
|
+
break if args.none?
|
217
|
+
arg = args.shift
|
218
|
+
if arg.kind_of? Symbol or arg.kind_of? Scope
|
219
|
+
thiz += arg
|
220
|
+
elsif arg.class == Hash
|
221
|
+
vars = arg
|
222
|
+
else
|
223
|
+
thiz += arg._
|
224
|
+
end
|
225
|
+
end
|
226
|
+
if block_given?
|
227
|
+
thiz = thiz.instance_eval(&block)
|
228
|
+
end
|
229
|
+
if vars
|
230
|
+
return thiz.with_variables(vars)
|
231
|
+
else
|
232
|
+
return thiz
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def with_variables(vars)
|
237
|
+
Scope.new(@path, @depth, variables.merge(vars), @default)
|
238
|
+
end
|
239
|
+
|
240
|
+
def with_default(default)
|
241
|
+
Scope.new(@path, @depth, @variables, default)
|
242
|
+
end
|
243
|
+
|
244
|
+
def inspect
|
245
|
+
return '(' + @path.map{|p| p.join '.'}.join(' , ') + ' '+depth.to_s+' v='+variables.inspect+' d='+default.inspect+')'
|
246
|
+
end
|
247
|
+
|
248
|
+
# Iterates over all possible paths.
|
249
|
+
# @yieldparam [Array] path
|
250
|
+
def each(&block)
|
251
|
+
@path.each(&block)
|
252
|
+
end
|
253
|
+
|
254
|
+
def humanization_key
|
255
|
+
return self
|
256
|
+
end
|
257
|
+
|
258
|
+
Root = self.new([[]],1)
|
259
|
+
None = self.new([],0)
|
260
|
+
Meta = self.new([[:__meta__]],1)
|
261
|
+
|
262
|
+
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# This program is free software: you can redistribute it and/or modify
|
3
|
+
# it under the terms of the Affero GNU General Public License as published by
|
4
|
+
# the Free Software Foundation, either version 3 of the License, or
|
5
|
+
# (at your option) any later version.
|
6
|
+
#
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
10
|
+
# GNU General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License
|
13
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
|
+
#
|
15
|
+
# (c) 2011 by Hannes Georg
|
16
|
+
#
|
17
|
+
require 'sync'
|
18
|
+
module Humanized
|
19
|
+
# A source lets you lookup,store and load data needed for humanization.
|
20
|
+
class Source
|
21
|
+
|
22
|
+
def initialize(data = {})
|
23
|
+
@source = data
|
24
|
+
@sync = Sync.new
|
25
|
+
@loaded = Set.new
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Loads a data-file or a dir of data-files.
|
30
|
+
#
|
31
|
+
# @param [String] path to a dir or file
|
32
|
+
# @option opts [Scope] :scope the root scope, where the loaded data will be stored ( default: L )
|
33
|
+
# @option opts [String] :grep a grep to be used when a dir is given ( default: '**/*.*' )
|
34
|
+
# @return self
|
35
|
+
def load(path,opts ={})
|
36
|
+
options = {:scope => Scope::Root, :grep => '**/*.*'}.update(opts)
|
37
|
+
if File.directory?(path)
|
38
|
+
f = File.join(path,options[:grep])
|
39
|
+
package('grep:' + f) do
|
40
|
+
Dir[f].each do |file|
|
41
|
+
package('file:'+file) do
|
42
|
+
data = self.read_file(file)
|
43
|
+
if data
|
44
|
+
xpath = file[path.size..(-1-File.extname(file).size)].split('/')
|
45
|
+
xpath.shift if xpath.first == ''
|
46
|
+
xscope = options[:scope]._(*xpath.map(&:to_sym))
|
47
|
+
self.store(xscope.first,data)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
else
|
53
|
+
package('file:'+path) do
|
54
|
+
data = self.read_file(path)
|
55
|
+
if data
|
56
|
+
self.store(options[:scope].first,data)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
return self
|
61
|
+
end
|
62
|
+
|
63
|
+
# Stores the given data on the base.
|
64
|
+
# @param [Object] data
|
65
|
+
# @see #store
|
66
|
+
def <<(data)
|
67
|
+
store([],data)
|
68
|
+
end
|
69
|
+
|
70
|
+
# This is method which will help you loading data once.
|
71
|
+
# It will load every package just one time.
|
72
|
+
# == Example
|
73
|
+
# source = Source.new
|
74
|
+
# 10.times do
|
75
|
+
# source.package('base') do |s|
|
76
|
+
# s << {:base => { :data => 'more data'}} # <= This data will be only loaded once!
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# @param [String] package name
|
81
|
+
# @yield self
|
82
|
+
# @yieldparam [Source] self
|
83
|
+
def package(name)
|
84
|
+
return nil if @loaded.include? name
|
85
|
+
@sync.synchronize(Sync::EX){
|
86
|
+
return nil if @loaded.include? name
|
87
|
+
yield(self)
|
88
|
+
@loaded << name
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
# Retrieves data
|
93
|
+
# @param [Scope, #each] scope a scope containing the paths to search for
|
94
|
+
# @return [String, Object, nil] data
|
95
|
+
def get(scope, default = nil)
|
96
|
+
scope.each do |path|
|
97
|
+
result = find(path, @source)
|
98
|
+
return result unless result.nil?
|
99
|
+
end
|
100
|
+
return default
|
101
|
+
end
|
102
|
+
|
103
|
+
# Stores data at the path
|
104
|
+
# @param [Array] path a path to store the data at
|
105
|
+
# @param [Object] data the data to store
|
106
|
+
def store(path ,data)
|
107
|
+
store!(path, data)
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
def store!(path ,str, hsh = @source)
|
113
|
+
@sync.synchronize(Sync::EX){
|
114
|
+
hshc = hsh
|
115
|
+
l = path.length - 1
|
116
|
+
if str.kind_of? Hash
|
117
|
+
l += 1
|
118
|
+
end
|
119
|
+
(0...l).each do |i|
|
120
|
+
a = path[i]
|
121
|
+
unless hshc.key?(a)
|
122
|
+
hshc[a] = {}
|
123
|
+
end
|
124
|
+
hshc = hshc[a]
|
125
|
+
while hshc.kind_of? Humanized::Ref
|
126
|
+
hshc = find(hshc, @source)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
if str.kind_of? Hash
|
130
|
+
hshc.deep_merge!(str)
|
131
|
+
else
|
132
|
+
hshc[path[l]] = str
|
133
|
+
end
|
134
|
+
return nil
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
def read_file(file)
|
139
|
+
ext = File.extname(file)[1..-1]
|
140
|
+
meth = "read_#{ext}".to_sym
|
141
|
+
if self.respond_to?(meth)
|
142
|
+
return self.send(meth,file)
|
143
|
+
else
|
144
|
+
warn "No reader found for #{ext}."
|
145
|
+
return nil
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def find(path, hsh)
|
150
|
+
hshc = hsh
|
151
|
+
l = path.length - 1
|
152
|
+
(0...l).each do |i|
|
153
|
+
a = path[i]
|
154
|
+
return nil unless hshc.key?(a)
|
155
|
+
hshc = hshc[a]
|
156
|
+
while hshc.kind_of? Humanized::Ref
|
157
|
+
hshc = find(hshc, @source)
|
158
|
+
end
|
159
|
+
return nil unless hshc.respond_to? :[]
|
160
|
+
end
|
161
|
+
hshc = hshc[path[l]]
|
162
|
+
while hshc.kind_of? Humanized::Ref
|
163
|
+
hshc = find(hshc, @source)
|
164
|
+
end
|
165
|
+
return hshc
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|