link2 0.1.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.
- data/MIT-LICENSE +20 -0
- data/README.textile +211 -0
- data/Rakefile +70 -0
- data/TODO +49 -0
- data/generators/link2/link2_generator.rb +11 -0
- data/generators/link2/templates/initializer.rb +14 -0
- data/lib/link2.rb +115 -0
- data/lib/link2/brain.rb +253 -0
- data/lib/link2/helpers.rb +57 -0
- data/lib/link2/i18n.rb +92 -0
- data/lib/link2/locales/en.yml +9 -0
- data/lib/link2/support.rb +31 -0
- data/lib/link2/version.rb +5 -0
- data/rails/init.rb +1 -0
- data/test/brain_test.rb +111 -0
- data/test/helpers_test.rb +125 -0
- data/test/i18n_test.rb +65 -0
- data/test/link2_test.rb +47 -0
- data/test/support/assertions_helper.rb +28 -0
- data/test/support/db_setup.rb +10 -0
- data/test/support/debug_helper.rb +18 -0
- data/test/support/substitutions_helper.rb +38 -0
- data/test/support_test.rb +19 -0
- data/test/test_helper.rb +54 -0
- metadata +117 -0
data/lib/link2/brain.rb
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Link2
|
4
|
+
module Brain
|
5
|
+
|
6
|
+
URL_PATH_REGEX = /\//
|
7
|
+
CLASS_INSTANCE_STRING = /\#\<.*\:0x.*\>/
|
8
|
+
|
9
|
+
LINK2_OPTION_KEYS = [:scope, :strong].freeze
|
10
|
+
LINK_TO_OPTION_KEYS = [:method, :confirm, :popup, :html_options].freeze
|
11
|
+
BUTTON_TO_OPTION_KEYS = [:method, :confirm, :disabled].freeze
|
12
|
+
IGNORED_OPTION_KEYS = (LINK2_OPTION_KEYS + LINK_TO_OPTION_KEYS + BUTTON_TO_OPTION_KEYS).uniq
|
13
|
+
|
14
|
+
POLYMORPHIC_OPTION_KEYS = [:action, :routing_type]
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
# The Link2 helpers brain: Extracts any additional known info about
|
19
|
+
# based on the specified helper arguments to make smart assumptions.
|
20
|
+
#
|
21
|
+
# NOTE: This method is quite messy, but the different conditionals makes
|
22
|
+
# it tricky to refactor much more for now until DSL is very settled.
|
23
|
+
# The comments give guidelines on what assumptions is being made in each case.
|
24
|
+
#
|
25
|
+
def link_to_args(*args)
|
26
|
+
html_options = args.extract_options!
|
27
|
+
url_options = args.extract_options!
|
28
|
+
args.unshift(capture(&block)) if block_given?
|
29
|
+
|
30
|
+
case args.size
|
31
|
+
when 0
|
32
|
+
raise "No arguments specified. A least specify action or url."
|
33
|
+
when 1
|
34
|
+
if args.first.is_a?(String)
|
35
|
+
if args.first =~ URL_PATH_REGEX
|
36
|
+
# link 'http://example.com' => link_to 'http://example.com', 'http://example.com'
|
37
|
+
label = url = args.shift
|
38
|
+
else
|
39
|
+
# link "Hello" => link_to 'Hello', '#'
|
40
|
+
url = ::Link2::DEFAULT_LINK
|
41
|
+
label = args.shift
|
42
|
+
end
|
43
|
+
elsif args.first.is_a?(Symbol)
|
44
|
+
# link :new => link_to I18n.t(:new, ...), new_{auto_detected_resource}_path
|
45
|
+
# link :back => link_to I18n.t(:back, ...), (session[:return_to] || :back)
|
46
|
+
action = args.shift
|
47
|
+
label = self.localized_label(action, url_options)
|
48
|
+
resource = nil # TODO: auto-detect resource.
|
49
|
+
url = self.url_for_args(action, resource, url_options)
|
50
|
+
elsif args.first.is_a?(Object)
|
51
|
+
# link @user => link_to I18n.t(:show, ...), user_path(@user)
|
52
|
+
# link [:admin, @user] => link_to I18n.t(:show, ...), admin_user_path(@user)
|
53
|
+
resource = args.shift
|
54
|
+
label, url = self.label_and_url_for_resource(resource, url_options)
|
55
|
+
else
|
56
|
+
raise "Invalid 1st argument: #{args.inspect}"
|
57
|
+
end
|
58
|
+
when 2
|
59
|
+
if args.first.is_a?(String)
|
60
|
+
if args.second.is_a?(String)
|
61
|
+
# link "Hello", hello_path => link_to "Hello", hello_path
|
62
|
+
label, url = args.slice!(0..1)
|
63
|
+
elsif self.resource_identifier_class?(args.second)
|
64
|
+
# link "New", :new => link_to "New", new_{auto_detected_resource}_path
|
65
|
+
# link "<<", :back => link_to "<<", (session[:return_to] || :back)
|
66
|
+
label, action = args.slice!(0..1)
|
67
|
+
resource = nil # TODO: auto-detect resource.
|
68
|
+
url = self.url_for_args(action, resource, url_options)
|
69
|
+
else
|
70
|
+
raise "Invalid 2nd argument: #{args.inspect}"
|
71
|
+
end
|
72
|
+
elsif args.first.is_a?(Symbol)
|
73
|
+
# TODO: Implement this:
|
74
|
+
raise ::Link2::NotImplementedYetError, "case link(:action, [...]) not yet supported. Need to refactor some stuff." if args.second.is_a?(Array)
|
75
|
+
# TODO: Cleanup.
|
76
|
+
if self.resource_identifier_class?(args.second)
|
77
|
+
# link :new, Post => link_to I18n.t(:new, ...), new_post_path
|
78
|
+
# link :edit, @post => link_to I18n.t(:edit, ...), edit_post_path(@post)
|
79
|
+
# link :show, [:admin, @user] => link_to I18n.t(:show, ...), admin_user_path(@user)
|
80
|
+
# link :back, root_path => link_to I18n.t(:back, ...), (session[:return_to] || :back)
|
81
|
+
action, resource = args.slice!(0..1)
|
82
|
+
label = self.localized_label(action, resource, url_options)
|
83
|
+
url = self.url_for_args(action, resource, url_options)
|
84
|
+
else
|
85
|
+
raise "Invalid 2nd argument: #{args.inspect}"
|
86
|
+
end
|
87
|
+
else
|
88
|
+
raise "Invalid 1st argument: #{args.inspect}"
|
89
|
+
end
|
90
|
+
when 3
|
91
|
+
if args.first.is_a?(String)
|
92
|
+
if args.second.is_a?(Symbol)
|
93
|
+
if self.resource_identifier_class?(args.third)
|
94
|
+
# link "New", :new, Post => link_to "New", new_post_path
|
95
|
+
# link "Edit", :edit, @post => link_to "Edit", edit_post_path(@post)
|
96
|
+
label, action, resource = args.slice!(0..2)
|
97
|
+
url = self.url_for_args(action, resource, url_options)
|
98
|
+
else
|
99
|
+
raise "Invalid 3rd argument: #{args.inspect}"
|
100
|
+
end
|
101
|
+
else
|
102
|
+
raise "Invalid 2nd argument: #{args.inspect}"
|
103
|
+
end
|
104
|
+
else
|
105
|
+
raise "Invalid 1st argument: #{args.inspect}"
|
106
|
+
end
|
107
|
+
else
|
108
|
+
raise "Invalid number of arguments: #{args.inspect}."
|
109
|
+
end
|
110
|
+
#[label, url, *((args << options) << html_options)]
|
111
|
+
#puts url_options.inspect
|
112
|
+
[label, url, *args]
|
113
|
+
rescue => e
|
114
|
+
raise ::ArgumentError, e
|
115
|
+
end
|
116
|
+
|
117
|
+
# Extracts a label and a url for a (polymorphic) resource.
|
118
|
+
# Partly based on - and accepts same options as - Rails core helper +polymorphic_url+.
|
119
|
+
#
|
120
|
+
# == Usage/Example:
|
121
|
+
#
|
122
|
+
# @post = Post.find(1)
|
123
|
+
#
|
124
|
+
# label_and_url_for_resource(@post)
|
125
|
+
# # => t(:show, ...), '/posts/1'
|
126
|
+
#
|
127
|
+
# label_and_url_for_resource([:admin, @post], :action => :edit)
|
128
|
+
# # => t(:show, ...), '/admin/posts/1/edit'
|
129
|
+
#
|
130
|
+
# label_and_url_for_resource(@post, :hello => 'World')
|
131
|
+
# # => t(:show, :hello => 'World', ...), '/posts/1'
|
132
|
+
#
|
133
|
+
# See documentation on +polymorphic_url+ for available core options.
|
134
|
+
#
|
135
|
+
def label_and_url_for_resource(resource, options = {})
|
136
|
+
resource.compact! if resource.is_a?(Array)
|
137
|
+
last_resource = [resource].flatten.last
|
138
|
+
url_for_options = options.slice!(*POLYMORPHIC_OPTION_KEYS).reverse_merge(:routing_type => :path)
|
139
|
+
i18n_options = options
|
140
|
+
|
141
|
+
# Skip any ugly default to_s-value.
|
142
|
+
custom_name = last_resource.to_s =~ CLASS_INSTANCE_STRING ? last_resource.class.human_name.downcase : last_resource.to_s
|
143
|
+
custom_name = last_resource.class.human_name.downcase if custom_name.blank?
|
144
|
+
|
145
|
+
i18n_options.merge!(:name => custom_name)
|
146
|
+
|
147
|
+
label = self.localized_label(:show, last_resource.class, i18n_options)
|
148
|
+
url = polymorphic_url(resource, url_for_options)
|
149
|
+
|
150
|
+
[label, url]
|
151
|
+
end
|
152
|
+
|
153
|
+
# Generates a proper URL based on specified arguments.
|
154
|
+
# Partly based on - and accepts same options as - Rails core helper +url_for+.
|
155
|
+
#
|
156
|
+
# Note: Overrides +:controller+, +:action+, and +:id+ options.
|
157
|
+
#
|
158
|
+
# == Usage/Example:
|
159
|
+
#
|
160
|
+
# @post = Post.find(1)
|
161
|
+
#
|
162
|
+
# url_for_args(:new, Post)
|
163
|
+
# # => '/posts/new'
|
164
|
+
#
|
165
|
+
# url_for_args(:edit, @post)
|
166
|
+
# # => '/posts/1/edit'
|
167
|
+
#
|
168
|
+
# url_for_args(:home)
|
169
|
+
# # => '/'
|
170
|
+
#
|
171
|
+
# See documentation on +url_for+ for available core options.
|
172
|
+
#
|
173
|
+
def url_for_args(*args)
|
174
|
+
options = args.extract_options!
|
175
|
+
action, resource = args
|
176
|
+
|
177
|
+
if resource.nil?
|
178
|
+
url = ::Link2.url_for_mapping(action, resource) rescue nil
|
179
|
+
if url
|
180
|
+
url
|
181
|
+
else
|
182
|
+
raise ::Link2::NotImplementedYetError,
|
183
|
+
"Resource needs to be specified for non-mapped actions; auto-detection of resource(s) not implemented yet."
|
184
|
+
end
|
185
|
+
elsif resource.is_a?(String)
|
186
|
+
resource
|
187
|
+
else
|
188
|
+
options[:controller] ||= self.controller_name_for_resource(resource)
|
189
|
+
options[:action] = action
|
190
|
+
options[:id] = resource.id if !resource.is_a?(Class) && self.record_class?(resource)
|
191
|
+
|
192
|
+
url_for(options.except(IGNORED_OPTION_KEYS))
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Helper for translating labels; merging any additional required translation info
|
197
|
+
# for the current template instance.
|
198
|
+
#
|
199
|
+
def localized_label(action, resource, options = {})
|
200
|
+
options.merge!(:controller => self.controller_name_for_resource(resource))
|
201
|
+
::Link2::I18n.t(action, resource, options)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Get controller name based for a specified resource.
|
205
|
+
#
|
206
|
+
# == Example/Usage:
|
207
|
+
#
|
208
|
+
# # Rails routing: map.resources :posts
|
209
|
+
#
|
210
|
+
# controller_name_for_resource
|
211
|
+
# # => "#{@template.controller.controller_name}"
|
212
|
+
#
|
213
|
+
# controller_name_for_resource(:post)
|
214
|
+
# # => "posts"
|
215
|
+
#
|
216
|
+
# controller_name_for_resource(Post)
|
217
|
+
# # => "posts"
|
218
|
+
#
|
219
|
+
# controller_name_for_resource(@post)
|
220
|
+
# # => "posts"
|
221
|
+
#
|
222
|
+
def controller_name_for_resource(resource = nil)
|
223
|
+
resource_class = ::Link2::Support.find_resource_class(resource)
|
224
|
+
if self.record_class?(resource_class)
|
225
|
+
resource_class.to_s.tableize # rescue nil
|
226
|
+
end || self.controller.controller_name
|
227
|
+
end
|
228
|
+
|
229
|
+
# Check if the specified object is a valid resource identifier class. Used
|
230
|
+
# for detecting current resource based on controller, action, etc.
|
231
|
+
#
|
232
|
+
def resource_identifier_class?(object)
|
233
|
+
object.is_a?(NilClass) || object.is_a?(Symbol) || self.record_class?(object)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Check if a specified objec is a record class type.
|
237
|
+
#
|
238
|
+
# == Usage/Examples:
|
239
|
+
#
|
240
|
+
# record_class?(ActiveRecord::Base)
|
241
|
+
# # => true
|
242
|
+
#
|
243
|
+
# record_class?(String)
|
244
|
+
# # => false
|
245
|
+
#
|
246
|
+
def record_class?(object_or_class)
|
247
|
+
return false if object_or_class == NilClass || object_or_class.is_a?(NilClass)
|
248
|
+
object_or_class = object_or_class.new if object_or_class.is_a?(Class)
|
249
|
+
object_or_class.respond_to?(:new_record?)
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Link2
|
4
|
+
module Helpers
|
5
|
+
|
6
|
+
include ::Link2::Brain
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
include JavascriptLinkHelpers
|
10
|
+
end
|
11
|
+
|
12
|
+
# Enhanced +link_to+ helper.
|
13
|
+
#
|
14
|
+
# TODO: Documentation for this helper.
|
15
|
+
#
|
16
|
+
def link(*args, &block)
|
17
|
+
args = self.link_to_args(*args)
|
18
|
+
link_to(*args)
|
19
|
+
end
|
20
|
+
alias :link2 :link
|
21
|
+
|
22
|
+
# Enhanced +button_to+ helper.
|
23
|
+
#
|
24
|
+
# == Usage/Examples:
|
25
|
+
#
|
26
|
+
# (See +link+ - identical except for passed +button_to+-options)
|
27
|
+
#
|
28
|
+
def button(*args, &block)
|
29
|
+
args = self.link_to_args(*args)
|
30
|
+
button_to(*args)
|
31
|
+
end
|
32
|
+
alias :button2 :button
|
33
|
+
|
34
|
+
# Rails 3-deprecations - unless +prototype_legacy_helper+-plugin.
|
35
|
+
|
36
|
+
module JavascriptLinkHelpers
|
37
|
+
def js_link(*args)
|
38
|
+
raise ::Link2::NotImplementedYetError
|
39
|
+
end
|
40
|
+
|
41
|
+
def js_button(*args)
|
42
|
+
raise ::Link2::NotImplementedYetError
|
43
|
+
end
|
44
|
+
|
45
|
+
def ajax_link(*args)
|
46
|
+
raise ::Link2::NotImplementedYetError
|
47
|
+
end
|
48
|
+
alias :remote_link :ajax_link
|
49
|
+
|
50
|
+
def ajax_button(*args)
|
51
|
+
raise ::Link2::NotImplementedYetError
|
52
|
+
end
|
53
|
+
alias :remote_button :ajax_button
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
data/lib/link2/i18n.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Link2
|
4
|
+
module I18n
|
5
|
+
|
6
|
+
ScopeInterpolationError = Class.new(::Link2::Error)
|
7
|
+
|
8
|
+
VALID_SCOPE_VARIABLES = [:controller, :action, :resource, :resources].freeze
|
9
|
+
INTERPOLATION_SYNTAX_PATTERN = /(\\)?\{\{([^\}]+)\}\}/
|
10
|
+
RESERVED_KEYS = ::I18n::Backend::Base::RESERVED_KEYS
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
# Helper method to lookup I18n keys based on a additionally known conditions;
|
15
|
+
# such as current action, model, etc.. This makes I18n translations more flexible
|
16
|
+
# and maintainable: Bottom-up approach; if no translation is found by first scope;
|
17
|
+
# try next scope, etc.
|
18
|
+
#
|
19
|
+
# == Scoped I18n lookup (default):
|
20
|
+
#
|
21
|
+
# 1. links.{{resource}}.{{action}}
|
22
|
+
# 2. links.{{action}}
|
23
|
+
# ...
|
24
|
+
#
|
25
|
+
def translate_with_scoping(action, resource, options = {})
|
26
|
+
raise ArgumentError, "At least action must be specified." unless action.present?
|
27
|
+
resource_name = self.localized_resource_class_name(resource)
|
28
|
+
i18n_options = options.merge(
|
29
|
+
:scope => nil,
|
30
|
+
:default => self.substituted_scopes_for(action, resource, options),
|
31
|
+
:resource => resource_name,
|
32
|
+
:resources => resource_name.pluralize
|
33
|
+
)
|
34
|
+
key = i18n_options[:default].shift
|
35
|
+
i18n_options[:default] << action.to_s.humanize
|
36
|
+
::I18n.t(key, i18n_options)
|
37
|
+
end
|
38
|
+
alias :translate :translate_with_scoping
|
39
|
+
alias :t :translate_with_scoping
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
# Pre-processeses Link2 I18n scopes by interpolating any scoping
|
44
|
+
# variables.
|
45
|
+
#
|
46
|
+
# == Usage/Examples:
|
47
|
+
#
|
48
|
+
# ::Link2.i18n_scopes = ['{{resources}}.links.{{action}}', '{{controller}}.links.{{action}}', 'links.{{action}}']
|
49
|
+
#
|
50
|
+
# substituted_scopes_for(:new, Post.new)
|
51
|
+
# # => Link2::I18n::ScopeInterpolationError
|
52
|
+
#
|
53
|
+
# substituted_scopes_for(:new, Post.new, :controller => 'admin')
|
54
|
+
# # => ['posts.links.new', 'admin.links.{new', 'links.new']
|
55
|
+
#
|
56
|
+
def substituted_scopes_for(action, resource, options = {})
|
57
|
+
resource_name = self.localized_resource_class_name(resource) # TODO: Should not be localized. Maybe use "model"/"models" to avoid confusion?
|
58
|
+
substitutions = options.merge(
|
59
|
+
:action => action.to_s.underscore,
|
60
|
+
:resource => resource_name,
|
61
|
+
:resources => resource_name.pluralize
|
62
|
+
)
|
63
|
+
|
64
|
+
scopes = ::Link2::i18n_scopes.collect do |i18n_scope|
|
65
|
+
i18n_key = i18n_scope.dup
|
66
|
+
i18n_key.gsub!(INTERPOLATION_SYNTAX_PATTERN, '%{\2}') # {{hello}} => %{hello}
|
67
|
+
begin
|
68
|
+
i18n_key = i18n_key % substitutions
|
69
|
+
rescue KeyError
|
70
|
+
raise ::Link2::I18n::ScopeInterpolationError,
|
71
|
+
"Contains a invalid scope-variable: #{i18n_key.inspect}. Valid scope-variables: #{VALID_SCOPE_VARIABLES.join(',')}"
|
72
|
+
# "key not found: #{i18n_key.inspect} where #{substitutions.collect { |k,v| "#{k}=#{v.inspect}"}.join(', ')}"
|
73
|
+
end
|
74
|
+
i18n_key.tr!('/', '.')
|
75
|
+
i18n_key.gsub!('..', '.')
|
76
|
+
i18n_key.to_sym
|
77
|
+
end
|
78
|
+
scopes
|
79
|
+
end
|
80
|
+
|
81
|
+
# Extracts a localized class name from a resource class/instance/identifier.
|
82
|
+
#
|
83
|
+
def localized_resource_class_name(resource)
|
84
|
+
resource_class = ::Link2::Support.find_resource_class(resource)
|
85
|
+
resource_name = resource_class.human_name rescue resource_class.to_s.humanize
|
86
|
+
resource_name.underscore
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Link2
|
4
|
+
module Support
|
5
|
+
|
6
|
+
class << self
|
7
|
+
# Get resource class based on name, object, or class.
|
8
|
+
#
|
9
|
+
# == Example/Usage:
|
10
|
+
#
|
11
|
+
# resource_class(:post), resource_class(@post), resource_class(Post)
|
12
|
+
# # => Post, Post, Post
|
13
|
+
#
|
14
|
+
def find_resource_class(arg)
|
15
|
+
if arg.is_a?(Symbol)
|
16
|
+
resource_class_name = arg.to_s.singularize.camelize
|
17
|
+
resource_class_name.constantize
|
18
|
+
elsif arg.is_a?(Class)
|
19
|
+
arg
|
20
|
+
elsif arg.is_a?(Object)
|
21
|
+
arg.class
|
22
|
+
else
|
23
|
+
arg
|
24
|
+
end
|
25
|
+
rescue
|
26
|
+
raise "No such class: #{resource_class_name}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|