rear 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +15 -0
  2. data/.travis.yml +7 -0
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile +20 -0
  5. data/LICENSE +19 -0
  6. data/README.md +101 -0
  7. data/Rakefile +79 -0
  8. data/assets/api.js +307 -0
  9. data/assets/bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css +8 -0
  10. data/assets/bootstrap-datetimepicker/js/bootstrap-datetimepicker.min.js +26 -0
  11. data/assets/bootstrap/css/bootstrap-responsive.min.css +9 -0
  12. data/assets/bootstrap/css/bootstrap.min.css +9 -0
  13. data/assets/bootstrap/img/glyphicons-halflings-white.png +0 -0
  14. data/assets/bootstrap/img/glyphicons-halflings.png +0 -0
  15. data/assets/bootstrap/js/bootstrap.min.js +6 -0
  16. data/assets/jquery.js +5 -0
  17. data/assets/noty/jquery.noty.js +520 -0
  18. data/assets/noty/layouts/top.js +34 -0
  19. data/assets/noty/layouts/topRight.js +43 -0
  20. data/assets/noty/promise.js +432 -0
  21. data/assets/noty/themes/default.js +156 -0
  22. data/assets/select2-bootstrap.css +86 -0
  23. data/assets/select2/select2-spinner.gif +0 -0
  24. data/assets/select2/select2.css +652 -0
  25. data/assets/select2/select2.min.js +22 -0
  26. data/assets/select2/select2.png +0 -0
  27. data/assets/select2/select2x2.png +0 -0
  28. data/assets/ui.css +75 -0
  29. data/assets/xhr.js +4 -0
  30. data/bin/rear +65 -0
  31. data/docs/Assocs.md +100 -0
  32. data/docs/Columns.md +404 -0
  33. data/docs/Deploy.md +62 -0
  34. data/docs/FileManager.md +75 -0
  35. data/docs/Filters.md +341 -0
  36. data/docs/Setup.md +201 -0
  37. data/lib/rear.rb +13 -0
  38. data/lib/rear/actions.rb +98 -0
  39. data/lib/rear/constants.rb +61 -0
  40. data/lib/rear/controller_setup.rb +249 -0
  41. data/lib/rear/helpers.rb +17 -0
  42. data/lib/rear/helpers/class.rb +46 -0
  43. data/lib/rear/helpers/columns.rb +68 -0
  44. data/lib/rear/helpers/filters.rb +147 -0
  45. data/lib/rear/helpers/generic.rb +73 -0
  46. data/lib/rear/helpers/order.rb +47 -0
  47. data/lib/rear/helpers/pager.rb +35 -0
  48. data/lib/rear/helpers/render.rb +37 -0
  49. data/lib/rear/home_controller.rb +10 -0
  50. data/lib/rear/input.rb +341 -0
  51. data/lib/rear/orm.rb +73 -0
  52. data/lib/rear/rear.rb +74 -0
  53. data/lib/rear/setup.rb +9 -0
  54. data/lib/rear/setup/associations.rb +33 -0
  55. data/lib/rear/setup/columns.rb +109 -0
  56. data/lib/rear/setup/filters.rb +314 -0
  57. data/lib/rear/setup/generic.rb +59 -0
  58. data/lib/rear/setup/menu.rb +39 -0
  59. data/lib/rear/templates/editor/ace.slim +7 -0
  60. data/lib/rear/templates/editor/assocs.slim +10 -0
  61. data/lib/rear/templates/editor/boolean.slim +5 -0
  62. data/lib/rear/templates/editor/bulk_edit.slim +75 -0
  63. data/lib/rear/templates/editor/checkbox.slim +5 -0
  64. data/lib/rear/templates/editor/ckeditor.slim +7 -0
  65. data/lib/rear/templates/editor/date.slim +9 -0
  66. data/lib/rear/templates/editor/datetime.slim +9 -0
  67. data/lib/rear/templates/editor/layout.slim +103 -0
  68. data/lib/rear/templates/editor/password.slim +1 -0
  69. data/lib/rear/templates/editor/radio.slim +5 -0
  70. data/lib/rear/templates/editor/select.slim +3 -0
  71. data/lib/rear/templates/editor/string.slim +1 -0
  72. data/lib/rear/templates/editor/text.slim +1 -0
  73. data/lib/rear/templates/editor/time.slim +9 -0
  74. data/lib/rear/templates/error.slim +36 -0
  75. data/lib/rear/templates/filters/boolean.slim +10 -0
  76. data/lib/rear/templates/filters/checkbox.slim +15 -0
  77. data/lib/rear/templates/filters/date.slim +10 -0
  78. data/lib/rear/templates/filters/datetime.slim +10 -0
  79. data/lib/rear/templates/filters/layout.slim +26 -0
  80. data/lib/rear/templates/filters/radio.slim +14 -0
  81. data/lib/rear/templates/filters/select.slim +9 -0
  82. data/lib/rear/templates/filters/string.slim +3 -0
  83. data/lib/rear/templates/filters/text.slim +3 -0
  84. data/lib/rear/templates/filters/time.slim +10 -0
  85. data/lib/rear/templates/home.slim +0 -0
  86. data/lib/rear/templates/layout.slim +78 -0
  87. data/lib/rear/templates/pager.slim +22 -0
  88. data/lib/rear/templates/pane/ace.slim +2 -0
  89. data/lib/rear/templates/pane/assocs.slim +62 -0
  90. data/lib/rear/templates/pane/boolean.slim +2 -0
  91. data/lib/rear/templates/pane/checkbox.slim +5 -0
  92. data/lib/rear/templates/pane/ckeditor.slim +2 -0
  93. data/lib/rear/templates/pane/date.slim +2 -0
  94. data/lib/rear/templates/pane/datetime.slim +2 -0
  95. data/lib/rear/templates/pane/layout.slim +111 -0
  96. data/lib/rear/templates/pane/password.slim +2 -0
  97. data/lib/rear/templates/pane/quickview.slim +21 -0
  98. data/lib/rear/templates/pane/radio.slim +5 -0
  99. data/lib/rear/templates/pane/select.slim +5 -0
  100. data/lib/rear/templates/pane/string.slim +2 -0
  101. data/lib/rear/templates/pane/text.slim +2 -0
  102. data/lib/rear/templates/pane/time.slim +2 -0
  103. data/lib/rear/utils.rb +288 -0
  104. data/rear.gemspec +27 -0
  105. data/test/helpers.rb +33 -0
  106. data/test/models/ar.rb +52 -0
  107. data/test/models/dm.rb +53 -0
  108. data/test/models/sq.rb +58 -0
  109. data/test/setup.rb +4 -0
  110. data/test/templates/adhoc/book/editor/name.slim +1 -0
  111. data/test/templates/adhoc/book/editor/string.slim +1 -0
  112. data/test/templates/adhoc/book/pane/name.slim +1 -0
  113. data/test/templates/adhoc/book/pane/string.slim +1 -0
  114. data/test/templates/shared/shared-templates/editor/string.slim +1 -0
  115. data/test/templates/shared/shared-templates/pane/string.slim +1 -0
  116. data/test/test__assocs.rb +249 -0
  117. data/test/test__columns.rb +269 -0
  118. data/test/test__crud.rb +81 -0
  119. data/test/test__custom_templates.rb +147 -0
  120. data/test/test__filters.rb +228 -0
  121. metadata +220 -0
@@ -0,0 +1,59 @@
1
+ module RearSetup
2
+
3
+ # tell controller to create a CRUD interface for given model
4
+ # opts and proc will be passed to Espresso's `crudify` helper.
5
+ #
6
+ # @param [Class] model
7
+ # @param [Hash] opts to be passed to `crudify` method
8
+ # @param [Proc] proc to be passed to `crudify` method
9
+ #
10
+ def model model = nil, opts = {}, &proc
11
+ return @__rear__model if @__rear__model || model.nil?
12
+ model = RearUtils.extract_constant(model)
13
+ RearUtils.is_orm?(model) ||
14
+ raise(ArgumentError, '"%s" is not a ActiveRecord/DataMapper/Sequel model' % model.inspect)
15
+ @__rear__model = model
16
+ @__rear__default_label = model.name.gsub(/\W/, '_').freeze
17
+ RearControllerSetup.crudify self, model, opts, &proc
18
+ end
19
+
20
+ def pkey key = nil
21
+ return unless model
22
+ @__rear__pkey = key if key
23
+ @__rear__pkey ||
24
+ raise(ArgumentError, "Was unable to automatically detect primary key for %s model.
25
+ Please set it manually via `pkey key_name`" % model)
26
+ end
27
+
28
+ def order_by *columns
29
+ @__rear__order = columns if columns.any?
30
+ @__rear__order
31
+ end
32
+
33
+ def items_per_page n = nil
34
+ @__rear__ipp = n.to_i if n
35
+ @__rear__ipp || 10
36
+ end
37
+ alias ipp items_per_page
38
+
39
+ # executed when new item created and when existing item updated
40
+ def on_save &proc
41
+ # const_get(:RearController).
42
+ before :save, &proc
43
+ end
44
+
45
+ # executed when existing item updated
46
+ def on_update &proc
47
+ before :update, &proc
48
+ end
49
+
50
+ def on_delete &proc
51
+ before :destroy, &proc
52
+ end
53
+ alias on_destroy on_delete
54
+
55
+ def readonly!
56
+ @__rear__readonly = true
57
+ end
58
+
59
+ end
@@ -0,0 +1,39 @@
1
+ module RearSetup
2
+
3
+ # by default all controllers are shown in main menu
4
+ # using the demodulized controller name.
5
+ #
6
+ # to use a custom label, set it via `menu_label` or its alias - `label`
7
+ # to hide a controller from menu set label to false.
8
+ def menu_label label = nil
9
+ @__rear__menu_label = label.freeze if label || label == false
10
+ @__rear__menu_label.nil? ? default_label : @__rear__menu_label
11
+ end
12
+ alias label menu_label
13
+
14
+ # by default controllers will be shown in the menu in the order they was defined.
15
+ # to have a controller shown before other ones set its menu_position to a higher number.
16
+ def menu_position position = nil
17
+ @__rear__menu_position = position.to_i if position
18
+ @__rear__menu_position || 0
19
+ end
20
+ alias position menu_position
21
+
22
+ # put current controller under some group.
23
+ #
24
+ # @example put Articles and Pages under Cms dropdown
25
+ # class Articles < E
26
+ # include Rear
27
+ # under :Cms
28
+ # end
29
+ # class Pages < E
30
+ # include Rear
31
+ # under :Cms
32
+ # end
33
+ #
34
+ def menu_group group = nil
35
+ @__rear__menu_group = group.to_s if group
36
+ @__rear__menu_group
37
+ end
38
+ alias under menu_group
39
+ end
@@ -0,0 +1,7 @@
1
+ - editor_id = "EditorFor" + column.string_name.capitalize
2
+ - a = {id: editor_id, name: column.name?}
3
+ textarea.input-block-level.text_editor *a *attrs(column, :editor) = value
4
+
5
+ - if defined?(EL::Ace)
6
+ - o = {readonly: column.readonly? || readonly?, snippets: column.snippets}
7
+ == ace editor_id, o
@@ -0,0 +1,10 @@
1
+ - assocs(assoc_type).each_pair do |assoc_name, assoc|
2
+ - remote_ctrl = associated_model_controller(assoc[:remote_model])
3
+ - remote_url = remote_ctrl.route(:reverse_assoc, self.class, assoc_type, assoc_name, item_id)
4
+ .tab-pane id="editor-tabs-#{assoc_name}"
5
+ javascript:
6
+ $(function(){
7
+ new Rear.Assoc('#{remote_url}', '##{assoc[:dom_id]}').load();
8
+ });
9
+ div id=(assoc[:dom_id])
10
+ div id=(assoc[:dom_id] + '_detached')
@@ -0,0 +1,5 @@
1
+ - COLUMNS__BOOLEAN_MAP.each_pair do |k,v|
2
+ - a = value == k ? {checked: true} : {}
3
+ label.radio.inline
4
+ input name=column.name? type="radio" value=k.inspect *a *attrs(column, :editor) = v
5
+ | &nbsp;
@@ -0,0 +1,75 @@
1
+
2
+ - crudifier_toggler = lambda do |column, dom_id, opts={}|
3
+ label.checkbox.inline class=('pull-right' unless opts[:left])
4
+ - name = 'rear-bulk_editor-crudifier_toggler[]'
5
+ input name=name type="checkbox" id=(dom_id + '-update_me') value=column
6
+ = 'Update %s' % (opts[:label] || column)
7
+
8
+ form.form-horizontal#bulk_editor-main_form
9
+ input.hidden type="hidden" name="rear-bulk_editor-items" value=@items
10
+ ul.nav.nav-tabs
11
+ li.active
12
+ a href="#editor-tabs-generic" data-toggle="tab"
13
+ i.icon-edit
14
+ | &nbsp;
15
+ = __rear__.label || __rear__.default_label
16
+
17
+ - assocs(:belongs_to).each_key do |a|
18
+ li
19
+ a href="#editor-tabs-#{a}" data-toggle="tab"
20
+ i.icon-tags
21
+ | &nbsp;
22
+ = a
23
+
24
+ .tab-content
25
+ .tab-pane.active#editor-tabs-generic
26
+
27
+ - rowed_columns = {}
28
+ - editor_columns.each do |column|
29
+ javascript:
30
+ $('.#{column.css_class}').change(function(){
31
+ $('##{column.dom_id}-update_me').prop('checked', true);
32
+ });
33
+ - next if rowed_columns[column.__id__]
34
+ - if column.row?
35
+ - if column.row.is_a?(String)
36
+ .row-fluid
37
+ .span style="text-align: center;"
38
+ h4.muted
39
+ = column.row
40
+
41
+ .row-fluid
42
+ - row_columns = editor_columns.select {|c| c.row == column.row}
43
+ - row_columns.each do |rc|
44
+ - self.column = rc
45
+ - rowed_columns[rc.__id__] = true
46
+ div class=('span%s' % (12 / row_columns.size).ceil)
47
+ .editor-column_container title=rc.label
48
+ .editor-column_value
49
+ == render_editor_column rc
50
+ - crudifier_toggler.call rc.name, rc.dom_id
51
+
52
+ - else
53
+ - self.column = column
54
+ .editor-column_container title=column.label
55
+ .editor-column_value
56
+ == render_editor_column column
57
+ - crudifier_toggler.call column.name, column.dom_id
58
+
59
+ - assocs(:belongs_to).each_pair do |assoc_name, assoc|
60
+ - remote_ctrl = associated_model_controller(assoc[:remote_model])
61
+ - remote_url = remote_ctrl.route(:reverse_assoc, self.class, :belongs_to, assoc_name, item_id)
62
+ .tab-pane id="editor-tabs-#{assoc_name}"
63
+ javascript:
64
+ $(function(){
65
+ new Rear.Assoc('#{remote_url}', '##{assoc[:dom_id]}').load_detached(function() {
66
+ $('.#{assoc[:dom_id]}_detached-assoc_toggler').change(function() {
67
+ $('##{assoc[:dom_id]}-update_me').prop('checked', true);
68
+ });
69
+ });
70
+ });
71
+
72
+ - column = assoc[:belongs_to_keys][:source]
73
+ - crudifier_toggler.call column, assoc[:dom_id], left: true, label: assoc[:name]
74
+
75
+ div id=(assoc[:dom_id] + '_detached')
@@ -0,0 +1,5 @@
1
+ - options.each_pair do |k,v|
2
+ - a = active_options.include?(k) ? {checked: true} : {}
3
+ label.checkbox.inline.editor-checkbox_container
4
+ input name=column.name? type="checkbox" value=k *a *attrs(column, :editor) = v
5
+ | &nbsp;
@@ -0,0 +1,7 @@
1
+ - editor_id = "EditorFor" + column.string_name.capitalize
2
+ - a = {id: editor_id, name: column.name?}
3
+ textarea.input-block-level.text_editor *a *attrs(column, :editor) = value
4
+
5
+ - if defined?(EL::CKE)
6
+ - o = {readonly: column.readonly? || readonly?, snippets: column.snippets}
7
+ == ckeditor editor_id, o.merge(column.ckeditor_opts)
@@ -0,0 +1,9 @@
1
+ - value = value.strftime('%Y-%m-%d') if value.kind_of?(Date)
2
+ - a = {name: column.name?, type: "text", value: value, "data-format" => "yyyy-MM-dd"}
3
+ .input-append.date id=column.name?
4
+ input.input-small *a *attrs(column, :editor)
5
+ span.add-on
6
+ i data-time-icon="icon-time" data-date-icon="icon-calendar" class=column.css_class
7
+ - if column.name?
8
+ javascript:
9
+ $(function() {$('##{column.name}').datetimepicker({pickTime: false})});
@@ -0,0 +1,9 @@
1
+ - value = value.strftime('%Y-%m-%d %H:%M:%S') if value.kind_of?(DateTime)
2
+ - a = {name: column.name?, type: "text", value: value, "data-format" => "yyyy-MM-dd hh:mm:ss"}
3
+ .input-append.date id=column.name?
4
+ input.input-medium *a *attrs(column, :editor)
5
+ span.add-on
6
+ i data-time-icon="icon-time" data-date-icon="icon-calendar" class=column.css_class
7
+ - if column.name?
8
+ javascript:
9
+ $(function() {$('##{column.name}').datetimepicker()});
@@ -0,0 +1,103 @@
1
+ javascript:
2
+ var crudifier = new Rear.CRUD(#{item_id}, '#{self[:crud]}', '#{self[:edit]}', #{readonly? ? true : false});
3
+
4
+ - if readonly?
5
+ javascript:
6
+ $(function(){
7
+ Rear.sticky_warn('ReadOnly Mode! Any updates will be discarded!')
8
+ });
9
+
10
+ .row-fluid
11
+ .span8
12
+
13
+ form.form-horizontal#editor-main_form
14
+ ul.nav.nav-tabs
15
+ li.active
16
+ a href="#editor-tabs-generic" data-toggle="tab"
17
+ i.icon-edit
18
+ | &nbsp;
19
+ = __rear__.label || __rear__.default_label
20
+
21
+ - assocs(:belongs_to).each_key do |a|
22
+ li
23
+ a href="#editor-tabs-#{a}" data-toggle="tab"
24
+ i.icon-tags
25
+ | &nbsp;
26
+ = a
27
+
28
+ - if item_id > 0
29
+ - assocs(:has_one, :has_many).each_key do |a|
30
+ li
31
+ a href="#editor-tabs-#{a}" data-toggle="tab"
32
+ i.icon-tags
33
+ | &nbsp;
34
+ = a
35
+ li
36
+ a href=route(:edit, 0)
37
+ span.badge.badge-warning
38
+ i.icon-plus
39
+ | &nbsp;New
40
+
41
+ - if item_id > 0
42
+ li
43
+ a onclick="if(confirm('This action can not be undone! Continue?')) { crudifier.delete('#{{route(pager_params)}}') } else { return false }" href="#"
44
+ span.badge.badge-important
45
+ i.icon-remove
46
+ | &nbsp;Delete
47
+
48
+ li
49
+ a.saveButton onclick="crudifier.save();" href="javascript:void(null);"
50
+ span.badge.badge-success#editor-save_badge
51
+ i.icon-check
52
+ | &nbsp;Save
53
+
54
+ .tab-content style="min-height: 50em;"
55
+ .tab-pane.active#editor-tabs-generic
56
+
57
+ - rowed_columns = []
58
+ - editor_columns.each do |column|
59
+ - next if rowed_columns.include?(column.__id__)
60
+ - if column.row?
61
+ - if column.row.is_a?(String)
62
+ .row-fluid
63
+ .span style="text-align: center;"
64
+ h4.muted
65
+ = column.row
66
+
67
+ .row-fluid
68
+ - columns = editor_columns.select {|c| c.row == column.row}
69
+ - columns.each do |rc|
70
+ - self.column = rc
71
+ - rowed_columns << rc.__id__
72
+ div class=('span%s' % (12 / columns.size).ceil)
73
+ .editor-column_container title=rc.label
74
+ .editor-column_value
75
+ == render_editor_column rc
76
+
77
+ - else
78
+ - self.column = column
79
+ .editor-column_container title=column.label
80
+ .editor-column_value
81
+ == render_editor_column column
82
+
83
+ == reander_p 'editor/assocs', assoc_type: :belongs_to
84
+ - if item_id > 0
85
+ - [:has_one, :has_many].each do |type|
86
+ == reander_p 'editor/assocs', assoc_type: type
87
+
88
+ hr
89
+ .row-fluid
90
+ .pull-right
91
+ a.btn.saveButton onclick="crudifier.save();" style="width: 10em;"
92
+ i.icon-check
93
+ | &nbsp;Save
94
+
95
+ .span4
96
+ #editor-navigation.hide_under_940px
97
+ javascript:
98
+ $(function(){
99
+ $.ajax({
100
+ type: 'GET',
101
+ url: '#{{route :quickview, pager_params.merge(selected: item_id.to_s)}}'
102
+ }).done(function(response){ $('#editor-navigation').html(response) });
103
+ });
@@ -0,0 +1 @@
1
+ input.input-block-level type="password" name=column.name? value=value *attrs(column, :editor)
@@ -0,0 +1,5 @@
1
+ - options.each_pair do |k,v|
2
+ - a = active_options.include?(k) ? {checked: true} : {}
3
+ label.radio.inline.editor-radio_container
4
+ input name=column.name? type="radio" value=k *a *attrs(column, :editor) = v
5
+ | &nbsp;
@@ -0,0 +1,3 @@
1
+ select.selectable name=column.name? *attrs(column, :editor)
2
+ - options.each_pair do |k,v|
3
+ option value=k selected=active_options.include?(k) = v
@@ -0,0 +1 @@
1
+ input.input-block-level type="text" name=column.name? value=value *attrs(column, :editor)
@@ -0,0 +1 @@
1
+ textarea.text_editor.input-block-level name=column.name? *attrs(column, :editor) = value
@@ -0,0 +1,9 @@
1
+ - value = value.strftime('%H:%M:%S') if value.kind_of?(Time)
2
+ - a = {name: column.name?, type: "text", value: value, "data-format" => "hh:mm:ss"}
3
+ .input-append.date id=column.name?
4
+ input.input-small *a *attrs(column, :editor)
5
+ span.add-on
6
+ i data-time-icon="icon-time" data-date-icon="icon-calendar" class=column.css_class
7
+ - if column.name?
8
+ javascript:
9
+ $(function() {$('##{column.name}').datetimepicker({pickDate: false})});
@@ -0,0 +1,36 @@
1
+ .alert.alert-error
2
+ h4 Something went wrong...
3
+ .text-info
4
+ | Please consider to report this at &nbsp;
5
+ a href="https://github.com/espresso/rear/issues" github.com/espresso/rear
6
+ h5 = error.first
7
+
8
+ h4.text-info Backtrace:
9
+ - error.each do |l|
10
+ div = l
11
+
12
+ h4.text-info Path:
13
+ = rq.path
14
+
15
+ h4.text-info Request Method:
16
+ = rq.request_method
17
+
18
+ h4.text-info Params:
19
+ table
20
+ - params.each_pair do |key,val|
21
+ tr
22
+ td
23
+ .pull-right.text-info
24
+ = '%s: ' % key
25
+ td
26
+ = val.is_a?(String) ? val : val.inspect
27
+
28
+ h4.text-info Env:
29
+ table
30
+ - env.each_pair do |key,val|
31
+ tr
32
+ td
33
+ .pull-right.text-info
34
+ = '%s: ' % key
35
+ td
36
+ = val.is_a?(String) ? val : val.inspect
@@ -0,0 +1,10 @@
1
+ .input-prepend.input-append
2
+ span.add-on
3
+ = setup[:label]
4
+
5
+ span.add-on
6
+ - COLUMNS__BOOLEAN_MAP.each_pair do |k,v|
7
+ - attrs = value == k.inspect ? {checked: true} : {}
8
+ label.radio
9
+ input name=name type="radio" value=k.inspect *attrs = v
10
+ | &nbsp;
@@ -0,0 +1,15 @@
1
+ - values = value.is_a?(Array) ? value : []
2
+ - if setup[:decorative?]
3
+ - attrs[:onclick] = "new Rear.Filters('%s', '%s').update();" % \
4
+ [dom_id, route(:html_filters, true, dom_id: dom_id)]
5
+
6
+ .input-prepend.input-append
7
+ span.add-on
8
+ = setup[:label]
9
+
10
+ span.add-on
11
+ - filter_setup_to_options(setup).each_pair do |v,l|
12
+ - attrs[:checked] = values.include?(v.to_s)
13
+ label.checkbox
14
+ input name=('%s[]' % name) type="checkbox" value=v *attrs = l
15
+ | &nbsp;