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.
@@ -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,9 @@
1
+ en:
2
+ links:
3
+ home: "Home"
4
+ back: "Back"
5
+ new: "New {{resource}}"
6
+ edit: "Edit {{resource}}"
7
+ delete: "Delete {{resource}}"
8
+ show: "Show {{resource}}"
9
+ index: "Index of {{resources}}"
@@ -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
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Link2
4
+ VERSION = '0.1.0'.freeze
5
+ end