slimmer 11.1.1 → 13.2.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 +5 -5
- data/CHANGELOG.md +25 -0
- data/README.md +1 -50
- data/Rakefile +1 -1
- data/lib/slimmer.rb +1 -9
- data/lib/slimmer/app.rb +19 -17
- data/lib/slimmer/cucumber.rb +0 -7
- data/lib/slimmer/headers.rb +14 -18
- data/lib/slimmer/processors/body_inserter.rb +2 -2
- data/lib/slimmer/processors/conditional_comment_mover.rb +0 -2
- data/lib/slimmer/processors/footer_remover.rb +1 -1
- data/lib/slimmer/processors/header_context_inserter.rb +3 -3
- data/lib/slimmer/processors/metadata_inserter.rb +1 -1
- data/lib/slimmer/processors/navigation_mover.rb +0 -1
- data/lib/slimmer/processors/search_parameter_inserter.rb +1 -1
- data/lib/slimmer/processors/search_path_setter.rb +1 -1
- data/lib/slimmer/processors/search_remover.rb +1 -1
- data/lib/slimmer/processors/tag_mover.rb +16 -9
- data/lib/slimmer/processors/title_inserter.rb +2 -2
- data/lib/slimmer/rspec.rb +0 -7
- data/lib/slimmer/skin.rb +11 -20
- data/lib/slimmer/test_templates/header_footer_only.html +2 -0
- data/lib/slimmer/test_templates/wrapper.html +2 -0
- data/lib/slimmer/version.rb +1 -1
- data/lib/tasks/slimmer.rake +1 -1
- metadata +7 -16
- data/lib/slimmer/component_resolver.rb +0 -26
- data/lib/slimmer/govuk_components.rb +0 -57
- data/lib/slimmer/i18n_backend.rb +0 -56
- data/lib/slimmer/local_component_resolver.rb +0 -19
- data/lib/slimmer/local_govuk_components.rb +0 -49
- data/lib/slimmer/network_component_resolver.rb +0 -45
- data/lib/slimmer/processors/report_a_problem_inserter.rb +0 -32
- data/lib/slimmer/test_helpers/govuk_components.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 966481420a36ac4cbaf811706d2ae142b9ce1556c6b8be10ca682766cbe8e2e6
|
4
|
+
data.tar.gz: 79cb54e5f8d0db751cbc40ff3acd6c573395fec2e13e4bb9e43021b93c69a3a4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd9b2d065c5cc805cc92f84728fb4990352030ac25ad5f8ad024b5794e90bc52b82c04f08eb1a7adfbd45abef17849e2ae977d95f579f3d184d290af5a9a4d3b
|
7
|
+
data.tar.gz: d846dd0e2ce3edcb9503708e0c162e5899530e988abf6513a785bef995cc2193df948718892e3200d4408e3ee79b34aead7a9ccec4af042b6214a98f480444b7
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
+
# 13.2.0
|
2
|
+
|
3
|
+
* Upgrade Ruby version to 2.6.5. (#234)
|
4
|
+
* Fix linting issues. (#234)
|
5
|
+
|
6
|
+
# 13.1.0
|
7
|
+
|
8
|
+
* Add `js-enabled` to body tag of test templates. (#226)
|
9
|
+
|
10
|
+
# 13.0.0
|
11
|
+
|
12
|
+
* Drop cache TTL to 60 seconds.
|
13
|
+
* BREAKING: Remove the component system, components are now consumed via the [govuk_publishing_components gem](https://github.com/alphagov/govuk_publishing_components)
|
14
|
+
|
15
|
+
# 12.1.0
|
16
|
+
|
17
|
+
* Make sure that the metatags defined in the application are inserted at the
|
18
|
+
top of the page. This means that third parties will see the custom metatags
|
19
|
+
like `og:image` first, and template tags like the default sharing image second (#218)
|
20
|
+
|
21
|
+
# 12.0.0
|
22
|
+
|
23
|
+
* Remove the "report a problem" feature. This is now being covered by the
|
24
|
+
feedback component (#213)
|
25
|
+
|
1
26
|
# 11.1.1
|
2
27
|
|
3
28
|
* Make the 'Inside Header Inserter' more resillient so that it doesn't throw
|
data/README.md
CHANGED
@@ -15,8 +15,7 @@ Slimmer provides a Railtie so no configuration is necessary.
|
|
15
15
|
|
16
16
|
## Caching
|
17
17
|
|
18
|
-
Slimmer makes HTTP requests to `static` for templates
|
19
|
-
are cached for 15 minutes. Slimmer uses `Rails.cache` for this.
|
18
|
+
Slimmer makes HTTP requests to `static` for templates. These are cached using `Rails.cache`.
|
20
19
|
|
21
20
|
## Asset tag helpers
|
22
21
|
|
@@ -82,54 +81,6 @@ YourApp::Application.configure do
|
|
82
81
|
end
|
83
82
|
```
|
84
83
|
|
85
|
-
## GOV.UK Components
|
86
|
-
|
87
|
-
To use [shared template components](https://govuk-static.herokuapp.com/component-guide) you need to include the GOV.UK component module:
|
88
|
-
|
89
|
-
```rb
|
90
|
-
class ApplicationController < ActionController::Base
|
91
|
-
include Slimmer::GovukComponents
|
92
|
-
end
|
93
|
-
```
|
94
|
-
|
95
|
-
This will make calls out to static when you try and render a partial prefixed with `govuk_component`:
|
96
|
-
|
97
|
-
```erb
|
98
|
-
<%= render partial: 'govuk_component/example_component' %>
|
99
|
-
```
|
100
|
-
|
101
|
-
You will need a copy of static running for the templates to be loaded from.
|
102
|
-
|
103
|
-
### Testing components
|
104
|
-
|
105
|
-
In test mode (when `Rails.env.test?` returns `true`), shared components are not
|
106
|
-
fetched from Static. Instead they are rendered as a dummy tag which contains a
|
107
|
-
JSON dump of the `locals` - the arguments passed to the component.
|
108
|
-
|
109
|
-
A test helper is included which returns a CSS selector for finding a given
|
110
|
-
component to assert that it was used. You can make it available in your tests
|
111
|
-
with:
|
112
|
-
|
113
|
-
```rb
|
114
|
-
require 'slimmer/test_helpers/govuk_components'
|
115
|
-
include Slimmer::TestHelpers::GovukComponents
|
116
|
-
```
|
117
|
-
|
118
|
-
And then assert that the component has been used:
|
119
|
-
|
120
|
-
```rb
|
121
|
-
page.should have_css(shared_component_selector('metadata'))
|
122
|
-
```
|
123
|
-
|
124
|
-
Or look for one of the arguments to the component which will have been
|
125
|
-
`JSON.dump`ed inside the tag:
|
126
|
-
|
127
|
-
```rb
|
128
|
-
within(shared_component_selector('title')) do
|
129
|
-
expect(page).to have_content(expected_title_text)
|
130
|
-
end
|
131
|
-
```
|
132
|
-
|
133
84
|
### Cucumber
|
134
85
|
|
135
86
|
Add the following code to features/support:
|
data/Rakefile
CHANGED
data/lib/slimmer.rb
CHANGED
@@ -7,7 +7,7 @@ require 'slimmer/version'
|
|
7
7
|
require 'slimmer/railtie' if defined? Rails
|
8
8
|
|
9
9
|
module Slimmer
|
10
|
-
CACHE_TTL =
|
10
|
+
CACHE_TTL = 60
|
11
11
|
|
12
12
|
def self.cache
|
13
13
|
@cache ||= defined?(Rails) ? Rails.cache : NoCache.new
|
@@ -27,13 +27,6 @@ module Slimmer
|
|
27
27
|
autoload :Headers, 'slimmer/headers'
|
28
28
|
autoload :HTTPClient, 'slimmer/http_client'
|
29
29
|
|
30
|
-
autoload :GovukComponents, 'slimmer/govuk_components'
|
31
|
-
autoload :LocalGovukComponents, 'slimmer/local_govuk_components'
|
32
|
-
autoload :ComponentResolver, 'slimmer/component_resolver'
|
33
|
-
autoload :NetworkComponentResolver, 'slimmer/network_component_resolver'
|
34
|
-
autoload :LocalComponentResolver, 'slimmer/local_component_resolver'
|
35
|
-
autoload :I18nBackend, 'slimmer/i18n_backend'
|
36
|
-
|
37
30
|
module Processors
|
38
31
|
autoload :BodyClassCopier, 'slimmer/processors/body_class_copier'
|
39
32
|
autoload :BodyInserter, 'slimmer/processors/body_inserter'
|
@@ -43,7 +36,6 @@ module Slimmer
|
|
43
36
|
autoload :HeaderContextInserter, 'slimmer/processors/header_context_inserter'
|
44
37
|
autoload :InsideHeaderInserter, 'slimmer/processors/inside_header_inserter'
|
45
38
|
autoload :NavigationMover, 'slimmer/processors/navigation_mover'
|
46
|
-
autoload :ReportAProblemInserter, 'slimmer/processors/report_a_problem_inserter'
|
47
39
|
autoload :SearchIndexSetter, 'slimmer/processors/search_index_setter'
|
48
40
|
autoload :SearchPathSetter, 'slimmer/processors/search_path_setter'
|
49
41
|
autoload :SearchParameterInserter, 'slimmer/processors/search_parameter_inserter'
|
data/lib/slimmer/app.rb
CHANGED
@@ -4,20 +4,21 @@ module Slimmer
|
|
4
4
|
class App
|
5
5
|
attr_accessor :logger
|
6
6
|
|
7
|
-
def initialize(app, *args
|
7
|
+
def initialize(app, *args)
|
8
8
|
options = args.first || {}
|
9
9
|
@app = app
|
10
10
|
|
11
11
|
logger = options[:logger] || NullLogger.instance
|
12
12
|
self.logger = logger
|
13
13
|
begin
|
14
|
-
if logger.level
|
14
|
+
if logger.level.zero? # Log set to debug level
|
15
15
|
unless options[:enable_debugging]
|
16
16
|
self.logger = logger.dup
|
17
17
|
self.logger.level = 1 # info
|
18
18
|
end
|
19
19
|
end
|
20
|
-
rescue NoMethodError #
|
20
|
+
rescue NoMethodError # rubocop:disable Lint/HandleExceptions
|
21
|
+
# In case logger doesn't respond_to? :level
|
21
22
|
end
|
22
23
|
|
23
24
|
if options.key? :template_path
|
@@ -60,7 +61,7 @@ module Slimmer
|
|
60
61
|
|
61
62
|
def skip_slimmer_param?(env)
|
62
63
|
skip = Rack::Request.new(env).params['skip_slimmer']
|
63
|
-
skip
|
64
|
+
skip && skip.to_i.positive?
|
64
65
|
end
|
65
66
|
|
66
67
|
def skip_slimmer_header?(response)
|
@@ -69,8 +70,9 @@ module Slimmer
|
|
69
70
|
|
70
71
|
def s(body)
|
71
72
|
return body.to_s unless body.respond_to?(:each)
|
73
|
+
|
72
74
|
b = ""
|
73
|
-
body.each {|a| b << a }
|
75
|
+
body.each { |a| b << a }
|
74
76
|
b
|
75
77
|
end
|
76
78
|
|
@@ -87,17 +89,17 @@ module Slimmer
|
|
87
89
|
GovukRequestId.value = env['HTTP_GOVUK_REQUEST_ID']
|
88
90
|
|
89
91
|
rewritten_body = case response.status
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
92
|
+
when 200
|
93
|
+
@skin.success request, response, s(response.body)
|
94
|
+
when 403
|
95
|
+
@skin.error '403', s(response.body), request.env
|
96
|
+
when 404
|
97
|
+
@skin.error '404', s(response.body), request.env
|
98
|
+
when 410
|
99
|
+
@skin.error '410', s(response.body), request.env
|
100
|
+
else
|
101
|
+
@skin.error '500', s(response.body), request.env
|
102
|
+
end
|
101
103
|
|
102
104
|
rewritten_body = [rewritten_body] unless rewritten_body.respond_to?(:each)
|
103
105
|
response.body = rewritten_body
|
@@ -107,7 +109,7 @@ module Slimmer
|
|
107
109
|
end
|
108
110
|
|
109
111
|
def strip_slimmer_headers(headers)
|
110
|
-
headers.reject {|k,
|
112
|
+
headers.reject { |k, _v| k =~ /\A#{Headers::HEADER_PREFIX}/ }
|
111
113
|
end
|
112
114
|
end
|
113
115
|
end
|
data/lib/slimmer/cucumber.rb
CHANGED
data/lib/slimmer/headers.rb
CHANGED
@@ -5,7 +5,7 @@ module Slimmer
|
|
5
5
|
InvalidHeader = Class.new(RuntimeError)
|
6
6
|
|
7
7
|
# @private
|
8
|
-
HEADER_PREFIX = "X-Slimmer"
|
8
|
+
HEADER_PREFIX = "X-Slimmer".freeze
|
9
9
|
|
10
10
|
# @private
|
11
11
|
SLIMMER_HEADER_MAPPING = {
|
@@ -13,7 +13,6 @@ module Slimmer
|
|
13
13
|
format: "Format",
|
14
14
|
page_owner: "Page-Owner",
|
15
15
|
organisations: "Organisations",
|
16
|
-
report_a_problem: "Report-a-Problem",
|
17
16
|
world_locations: "World-Locations",
|
18
17
|
result_count: "Result-Count",
|
19
18
|
search_parameters: "Search-Parameters",
|
@@ -21,43 +20,40 @@ module Slimmer
|
|
21
20
|
skip: "Skip",
|
22
21
|
template: "Template",
|
23
22
|
remove_search: "Remove-Search",
|
24
|
-
}
|
23
|
+
}.freeze
|
25
24
|
|
26
25
|
# @private
|
27
|
-
APPLICATION_NAME_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:application_name]}"
|
26
|
+
APPLICATION_NAME_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:application_name]}".freeze
|
28
27
|
|
29
28
|
# @private
|
30
|
-
FORMAT_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:format]}"
|
29
|
+
FORMAT_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:format]}".freeze
|
31
30
|
|
32
31
|
# @private
|
33
|
-
ORGANISATIONS_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:organisations]}"
|
32
|
+
ORGANISATIONS_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:organisations]}".freeze
|
34
33
|
|
35
34
|
# @private
|
36
|
-
|
35
|
+
WORLD_LOCATIONS_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:world_locations]}".freeze
|
37
36
|
|
38
37
|
# @private
|
39
|
-
|
38
|
+
PAGE_OWNER_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:page_owner]}".freeze
|
40
39
|
|
41
40
|
# @private
|
42
|
-
|
41
|
+
RESULT_COUNT_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:result_count]}".freeze
|
43
42
|
|
44
43
|
# @private
|
45
|
-
|
44
|
+
SEARCH_PATH_HEADER = "#{HEADER_PREFIX}-Search-Path".freeze
|
46
45
|
|
47
46
|
# @private
|
48
|
-
|
47
|
+
SEARCH_PARAMETERS_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:search_parameters]}".freeze
|
49
48
|
|
50
49
|
# @private
|
51
|
-
|
50
|
+
SKIP_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:skip]}".freeze
|
52
51
|
|
53
52
|
# @private
|
54
|
-
|
53
|
+
TEMPLATE_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:template]}".freeze
|
55
54
|
|
56
55
|
# @private
|
57
|
-
|
58
|
-
|
59
|
-
# @private
|
60
|
-
REMOVE_SEARCH_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:remove_search]}"
|
56
|
+
REMOVE_SEARCH_HEADER = "#{HEADER_PREFIX}-#{SLIMMER_HEADER_MAPPING[:remove_search]}".freeze
|
61
57
|
|
62
58
|
# Set the "slimmer headers" to configure the page
|
63
59
|
#
|
@@ -67,7 +63,6 @@ module Slimmer
|
|
67
63
|
# @option hash [String] organisations
|
68
64
|
# @option hash [String] page_owner
|
69
65
|
# @option hash [String] remove_search
|
70
|
-
# @option hash [String] report_a_problem
|
71
66
|
# @option hash [String] result_count
|
72
67
|
# @option hash [String] search_parameters
|
73
68
|
# @option hash [String] section
|
@@ -76,6 +71,7 @@ module Slimmer
|
|
76
71
|
# @option hash [String] world_locations
|
77
72
|
def set_slimmer_headers(hash)
|
78
73
|
raise InvalidHeader if (hash.keys - SLIMMER_HEADER_MAPPING.keys).any?
|
74
|
+
|
79
75
|
SLIMMER_HEADER_MAPPING.each do |hash_key, header_suffix|
|
80
76
|
value = hash[hash_key]
|
81
77
|
headers["#{HEADER_PREFIX}-#{header_suffix}"] = value.to_s if value
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module Slimmer::Processors
|
2
2
|
class BodyInserter
|
3
|
-
def initialize(source_id='wrapper', destination_id='wrapper')
|
3
|
+
def initialize(source_id = 'wrapper', destination_id = 'wrapper')
|
4
4
|
@source_selector = '#' + source_id
|
5
5
|
@destination_selector = '#' + destination_id
|
6
6
|
end
|
7
7
|
|
8
|
-
def filter(src,dest)
|
8
|
+
def filter(src, dest)
|
9
9
|
body = Nokogiri::HTML.fragment(src.at_css(@source_selector).to_html)
|
10
10
|
dest.at_css(@destination_selector).replace(body)
|
11
11
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module Slimmer::Processors
|
2
2
|
class HeaderContextInserter
|
3
|
-
def initialize(path='.header-context')
|
3
|
+
def initialize(path = '.header-context')
|
4
4
|
@path = path
|
5
5
|
end
|
6
6
|
|
7
|
-
def filter(src,dest)
|
8
|
-
if dest.at_css(@path) && replacement = src.at_css(@path)
|
7
|
+
def filter(src, dest)
|
8
|
+
if dest.at_css(@path) && (replacement = src.at_css(@path))
|
9
9
|
header_context = src.fragment(replacement)
|
10
10
|
dest.at_css(@path).replace(header_context)
|
11
11
|
end
|
@@ -6,7 +6,7 @@ module Slimmer::Processors
|
|
6
6
|
@response = response
|
7
7
|
end
|
8
8
|
|
9
|
-
def filter(
|
9
|
+
def filter(_content_document, page_template)
|
10
10
|
search_form = page_template.at_css('form#search')
|
11
11
|
if search_parameters && search_form
|
12
12
|
search_parameters.each_pair do |name, value|
|
@@ -4,7 +4,7 @@ module Slimmer::Processors
|
|
4
4
|
@response = response
|
5
5
|
end
|
6
6
|
|
7
|
-
def filter(
|
7
|
+
def filter(_content_document, page_template)
|
8
8
|
if search_scope && page_template.at_css('form#search')
|
9
9
|
page_template.at_css('form#search').attributes["action"].value = search_scope
|
10
10
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
module Slimmer::Processors
|
2
2
|
class TagMover
|
3
|
-
def filter(src,dest)
|
4
|
-
move_tags(src, dest, 'script', :
|
5
|
-
move_tags(src, dest, 'link', :
|
6
|
-
move_tags(src, dest, 'meta', :
|
7
|
-
move_tags(src, dest, 'meta', :
|
3
|
+
def filter(src, dest)
|
4
|
+
move_tags(src, dest, 'script', dest_node: 'body', keys: %w(src inner_html))
|
5
|
+
move_tags(src, dest, 'link', must_have: %w[href])
|
6
|
+
move_tags(src, dest, 'meta', must_have: %w(name content), keys: %w[name content http-equiv], insertion_location: :top)
|
7
|
+
move_tags(src, dest, 'meta', must_have: %w(property content), keys: %w(property content), insertion_location: :top)
|
8
8
|
end
|
9
9
|
|
10
10
|
def include_tag?(node, min_attrs)
|
@@ -12,17 +12,19 @@ module Slimmer::Processors
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def tag_fingerprint(node, attrs)
|
15
|
-
attrs.collect do |attr_name|
|
15
|
+
collected_attrs = attrs.collect do |attr_name|
|
16
16
|
if attr_name == 'inner_html'
|
17
17
|
node.content
|
18
18
|
else
|
19
19
|
node.has_attribute?(attr_name) ? node.attr(attr_name) : nil
|
20
20
|
end
|
21
|
-
end
|
21
|
+
end
|
22
|
+
|
23
|
+
collected_attrs.compact.sort
|
22
24
|
end
|
23
25
|
|
24
26
|
def wrap_node(src, node)
|
25
|
-
if node.previous_sibling.to_s =~ /<!--\[if[^\]]+\]><!-->/
|
27
|
+
if node.previous_sibling.to_s =~ /<!--\[if[^\]]+\]><!-->/ && node.next_sibling.to_s == '<!--<![endif]-->'
|
26
28
|
node = Nokogiri::XML::NodeSet.new(src, [node.previous_sibling, node, node.next_sibling])
|
27
29
|
end
|
28
30
|
node
|
@@ -40,7 +42,12 @@ module Slimmer::Processors
|
|
40
42
|
if include_tag?(node, min_attrs) && !already_there.include?(tag_fingerprint(node, comparison_attrs))
|
41
43
|
node = wrap_node(src, node)
|
42
44
|
node.remove
|
43
|
-
|
45
|
+
|
46
|
+
if opts[:insertion_location] == :top
|
47
|
+
dest.at_xpath("/html/#{dest_node}").prepend_child(node)
|
48
|
+
else
|
49
|
+
dest.at_xpath("/html/#{dest_node}") << node
|
50
|
+
end
|
44
51
|
end
|
45
52
|
end
|
46
53
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
module Slimmer::Processors
|
2
2
|
class TitleInserter
|
3
|
-
def filter(src,dest)
|
3
|
+
def filter(src, dest)
|
4
4
|
title = src.at_css('head title')
|
5
5
|
head = dest.at_xpath('/html/head')
|
6
6
|
if head && title
|
7
|
-
insert_title(title,head)
|
7
|
+
insert_title(title, head)
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
data/lib/slimmer/rspec.rb
CHANGED
@@ -2,10 +2,3 @@ require 'rspec/core'
|
|
2
2
|
|
3
3
|
require 'slimmer'
|
4
4
|
require 'slimmer/test'
|
5
|
-
require 'slimmer/test_helpers/govuk_components'
|
6
|
-
|
7
|
-
RSpec.configure do |config|
|
8
|
-
config.include Slimmer::TestHelpers::GovukComponents
|
9
|
-
|
10
|
-
config.before { stub_shared_component_locales }
|
11
|
-
end
|
data/lib/slimmer/skin.rb
CHANGED
@@ -10,7 +10,7 @@ module Slimmer
|
|
10
10
|
@asset_host = options[:asset_host]
|
11
11
|
|
12
12
|
@logger = options[:logger] || NullLogger.instance
|
13
|
-
@strict = options[:strict] ||
|
13
|
+
@strict = options[:strict] || %w{development test}.include?(ENV['RACK_ENV'])
|
14
14
|
end
|
15
15
|
|
16
16
|
def template(template_name)
|
@@ -34,15 +34,15 @@ module Slimmer
|
|
34
34
|
"#{host}templates/#{template_name}.html.erb"
|
35
35
|
end
|
36
36
|
|
37
|
-
def report_parse_errors_if_strict!(nokogiri_doc,
|
37
|
+
def report_parse_errors_if_strict!(nokogiri_doc, _description_for_error_message)
|
38
38
|
nokogiri_doc
|
39
39
|
end
|
40
40
|
|
41
41
|
def parse_html(html, description_for_error_message)
|
42
42
|
doc = Nokogiri::HTML.parse(html)
|
43
43
|
if strict
|
44
|
-
errors = doc.errors.select
|
45
|
-
if errors.
|
44
|
+
errors = doc.errors.select(&:error?).reject { |e| ignorable?(e) }
|
45
|
+
if !errors.empty?
|
46
46
|
error = errors.first
|
47
47
|
message = "In #{description_for_error_message}: '#{error.message}' at line #{error.line} col #{error.column} (code #{error.code}).\n"
|
48
48
|
message << "Add ?skip_slimmer=1 to the url to show the raw backend request.\n\n"
|
@@ -59,7 +59,7 @@ module Slimmer
|
|
59
59
|
lines = [""] + html.split("\n")
|
60
60
|
from = [1, error.line - context_size].max
|
61
61
|
to = [lines.size - 1, error.line + context_size].min
|
62
|
-
context = (from..to).zip(lines[from..to]).map {|lineno, line| "%4d: %s" % [lineno, line] }
|
62
|
+
context = (from..to).zip(lines[from..to]).map { |lineno, line| "%4d: %s" % [lineno, line] } # rubocop:disable Style/FormatStringToken
|
63
63
|
marker = " " * (error.column - 1) + "-----v"
|
64
64
|
context.insert(context_size, marker)
|
65
65
|
context.join("\n")
|
@@ -70,7 +70,7 @@ module Slimmer
|
|
70
70
|
ignorable_codes.include?(error.code) || error.message.match(/Element script embeds close tag/) || error.message.match(/Unexpected end tag : noscript/)
|
71
71
|
end
|
72
72
|
|
73
|
-
def process(processors,body,template,
|
73
|
+
def process(processors, body, template, _rack_env)
|
74
74
|
logger.debug "Slimmer: starting skinning process"
|
75
75
|
src = parse_html(body.to_s, "backend response")
|
76
76
|
dest = parse_html(template, "template")
|
@@ -94,27 +94,18 @@ module Slimmer
|
|
94
94
|
def success(source_request, response, body)
|
95
95
|
wrapper_id = options[:wrapper_id] || 'wrapper'
|
96
96
|
|
97
|
-
# The variables set in the source request might not be encoded as UTF-8,
|
98
|
-
# Unicorn, for instance, will set them to be ASCII. This shouldn't matter
|
99
|
-
# normally because URI's should have UTF-8 characters percent encoded,
|
100
|
-
# but not all agents follow this.
|
101
|
-
request_url = source_request.url.force_encoding(Encoding::UTF_8)
|
102
97
|
processors = [
|
103
|
-
Processors::TitleInserter.new
|
104
|
-
Processors::TagMover.new
|
98
|
+
Processors::TitleInserter.new,
|
99
|
+
Processors::TagMover.new,
|
105
100
|
Processors::NavigationMover.new(self),
|
106
|
-
Processors::ConditionalCommentMover.new
|
101
|
+
Processors::ConditionalCommentMover.new,
|
107
102
|
Processors::BodyInserter.new(wrapper_id),
|
108
103
|
Processors::BodyClassCopier.new,
|
109
104
|
Processors::InsideHeaderInserter.new,
|
110
|
-
Processors::HeaderContextInserter.new
|
105
|
+
Processors::HeaderContextInserter.new,
|
111
106
|
Processors::MetadataInserter.new(response, options[:app_name]),
|
112
107
|
Processors::SearchParameterInserter.new(response),
|
113
108
|
Processors::SearchPathSetter.new(response),
|
114
|
-
Processors::ReportAProblemInserter.new(self,
|
115
|
-
request_url,
|
116
|
-
response.headers,
|
117
|
-
wrapper_id),
|
118
109
|
Processors::SearchRemover.new(response.headers),
|
119
110
|
]
|
120
111
|
|
@@ -124,7 +115,7 @@ module Slimmer
|
|
124
115
|
|
125
116
|
def error(template_name, body, rack_env)
|
126
117
|
processors = [
|
127
|
-
Processors::TitleInserter.new
|
118
|
+
Processors::TitleInserter.new
|
128
119
|
]
|
129
120
|
process(processors, body, template(template_name), rack_env)
|
130
121
|
end
|
data/lib/slimmer/version.rb
CHANGED
data/lib/tasks/slimmer.rake
CHANGED
@@ -6,7 +6,7 @@ namespace :slimmer do
|
|
6
6
|
path_to_static = "../static/public"
|
7
7
|
path_to_public = "public"
|
8
8
|
commands = ["cd #{path_to_public}"]
|
9
|
-
|
9
|
+
Dir.glob("../static/public/*") { |f|
|
10
10
|
commands << "ln -s #{path_to_static}/#{f}"
|
11
11
|
}
|
12
12
|
commands << ["cd .."]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slimmer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 13.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GOV.UK Dev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-10-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -184,14 +184,14 @@ dependencies:
|
|
184
184
|
requirements:
|
185
185
|
- - '='
|
186
186
|
- !ruby/object:Gem::Version
|
187
|
-
version:
|
187
|
+
version: 3.5.0
|
188
188
|
type: :development
|
189
189
|
prerelease: false
|
190
190
|
version_requirements: !ruby/object:Gem::Requirement
|
191
191
|
requirements:
|
192
192
|
- - '='
|
193
193
|
- !ruby/object:Gem::Version
|
194
|
-
version:
|
194
|
+
version: 3.5.0
|
195
195
|
- !ruby/object:Gem::Dependency
|
196
196
|
name: timecop
|
197
197
|
requirement: !ruby/object:Gem::Requirement
|
@@ -212,14 +212,14 @@ dependencies:
|
|
212
212
|
requirements:
|
213
213
|
- - "~>"
|
214
214
|
- !ruby/object:Gem::Version
|
215
|
-
version:
|
215
|
+
version: 3.11.5
|
216
216
|
type: :development
|
217
217
|
prerelease: false
|
218
218
|
version_requirements: !ruby/object:Gem::Requirement
|
219
219
|
requirements:
|
220
220
|
- - "~>"
|
221
221
|
- !ruby/object:Gem::Version
|
222
|
-
version:
|
222
|
+
version: 3.11.5
|
223
223
|
description: Rack middleware for skinning pages using a specific template
|
224
224
|
email:
|
225
225
|
- govuk-dev@digital.cabinet-office.gov.uk
|
@@ -234,16 +234,10 @@ files:
|
|
234
234
|
- bin/render_slimmer_error
|
235
235
|
- lib/slimmer.rb
|
236
236
|
- lib/slimmer/app.rb
|
237
|
-
- lib/slimmer/component_resolver.rb
|
238
237
|
- lib/slimmer/cucumber.rb
|
239
|
-
- lib/slimmer/govuk_components.rb
|
240
238
|
- lib/slimmer/govuk_request_id.rb
|
241
239
|
- lib/slimmer/headers.rb
|
242
240
|
- lib/slimmer/http_client.rb
|
243
|
-
- lib/slimmer/i18n_backend.rb
|
244
|
-
- lib/slimmer/local_component_resolver.rb
|
245
|
-
- lib/slimmer/local_govuk_components.rb
|
246
|
-
- lib/slimmer/network_component_resolver.rb
|
247
241
|
- lib/slimmer/processors/body_class_copier.rb
|
248
242
|
- lib/slimmer/processors/body_inserter.rb
|
249
243
|
- lib/slimmer/processors/conditional_comment_mover.rb
|
@@ -252,7 +246,6 @@ files:
|
|
252
246
|
- lib/slimmer/processors/inside_header_inserter.rb
|
253
247
|
- lib/slimmer/processors/metadata_inserter.rb
|
254
248
|
- lib/slimmer/processors/navigation_mover.rb
|
255
|
-
- lib/slimmer/processors/report_a_problem_inserter.rb
|
256
249
|
- lib/slimmer/processors/search_parameter_inserter.rb
|
257
250
|
- lib/slimmer/processors/search_path_setter.rb
|
258
251
|
- lib/slimmer/processors/search_remover.rb
|
@@ -263,7 +256,6 @@ files:
|
|
263
256
|
- lib/slimmer/skin.rb
|
264
257
|
- lib/slimmer/template.rb
|
265
258
|
- lib/slimmer/test.rb
|
266
|
-
- lib/slimmer/test_helpers/govuk_components.rb
|
267
259
|
- lib/slimmer/test_templates/header_footer_only.html
|
268
260
|
- lib/slimmer/test_templates/proposition_menu.html
|
269
261
|
- lib/slimmer/test_templates/wrapper.html
|
@@ -289,8 +281,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
289
281
|
- !ruby/object:Gem::Version
|
290
282
|
version: '0'
|
291
283
|
requirements: []
|
292
|
-
|
293
|
-
rubygems_version: 2.5.1
|
284
|
+
rubygems_version: 3.0.3
|
294
285
|
signing_key:
|
295
286
|
specification_version: 4
|
296
287
|
summary: Thinner than the skinner
|
@@ -1,26 +0,0 @@
|
|
1
|
-
module Slimmer
|
2
|
-
class ComponentResolver < ::ActionView::Resolver
|
3
|
-
TEST_TAG_NAME = 'test-govuk-component'
|
4
|
-
|
5
|
-
def find_templates(name, prefix, partial, details, outside_app_allowed = false)
|
6
|
-
return [] unless prefix == 'govuk_component'
|
7
|
-
template_path = [prefix, name].join('/')
|
8
|
-
details = {
|
9
|
-
:format => 'text/html',
|
10
|
-
:updated_at => Time.now,
|
11
|
-
:virtual_path => template_path
|
12
|
-
}
|
13
|
-
|
14
|
-
[ActionView::Template.new(template_body(template_path), template_path, erb_handler, details)]
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
def erb_handler
|
19
|
-
@erb_handler ||= ActionView::Template.registered_template_handler(:erb)
|
20
|
-
end
|
21
|
-
|
22
|
-
def template_body(_template_path)
|
23
|
-
raise NotImplementedError, "Use NetworkComponentResolver or LocalComponentResolver"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,57 +0,0 @@
|
|
1
|
-
module Slimmer
|
2
|
-
# @api public
|
3
|
-
#
|
4
|
-
# Include this module to add the GOV.UK Components to your app.
|
5
|
-
# @example
|
6
|
-
# class ApplicationController < ActionController::Base
|
7
|
-
# include Slimmer::GovukComponents
|
8
|
-
# end
|
9
|
-
#
|
10
|
-
# # In your views:
|
11
|
-
#
|
12
|
-
# <%= render partial: 'govuk_component/example_component' %>
|
13
|
-
module GovukComponents
|
14
|
-
def self.included into
|
15
|
-
into.before_action :add_govuk_components
|
16
|
-
end
|
17
|
-
|
18
|
-
# @private
|
19
|
-
def add_govuk_components
|
20
|
-
append_view_path GovukComponents.expiring_resolver_cache.resolver
|
21
|
-
|
22
|
-
return if slimmer_backend_included?
|
23
|
-
I18n.backend = I18n::Backend::Chain.new(I18n.backend, Slimmer::I18nBackend.new)
|
24
|
-
end
|
25
|
-
|
26
|
-
# @private
|
27
|
-
def self.expiring_resolver_cache
|
28
|
-
@expiring_resolver_cache ||= TimedExpirationResolverCache.new
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def slimmer_backend_included?
|
34
|
-
I18n.backend.is_a?(I18n::Backend::Chain) &&
|
35
|
-
I18n.backend.backends.any? { |b| b.is_a? Slimmer::I18nBackend }
|
36
|
-
end
|
37
|
-
|
38
|
-
# Slimmer::ComponentResolver instantiates a lot of large objects and leaks
|
39
|
-
# memory. This class will cache the resolver so that it doesn't have to
|
40
|
-
# create new ActionView::Template objects for each request. The cache is
|
41
|
-
# timed to allow frontends to pick up changes made to components in `static`.
|
42
|
-
class TimedExpirationResolverCache
|
43
|
-
def initialize
|
44
|
-
@cache_last_reset = Time.now
|
45
|
-
end
|
46
|
-
|
47
|
-
def resolver
|
48
|
-
if (@cache_last_reset + Slimmer::CACHE_TTL) < Time.now
|
49
|
-
@resolver = nil
|
50
|
-
@cache_last_reset = Time.now
|
51
|
-
end
|
52
|
-
|
53
|
-
@resolver ||= Slimmer::NetworkComponentResolver.new
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
data/lib/slimmer/i18n_backend.rb
DELETED
@@ -1,56 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'i18n'
|
3
|
-
|
4
|
-
module Slimmer
|
5
|
-
class I18nBackend
|
6
|
-
include I18n::Backend::Base, I18n::Backend::Flatten
|
7
|
-
|
8
|
-
def available_locales
|
9
|
-
Slimmer.cache.fetch("available_locales", expires_in: Slimmer::CACHE_TTL) do
|
10
|
-
locale_json = fetch(static_locales_url)
|
11
|
-
JSON.parse(locale_json).map(&:to_sym)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def lookup(locale, key, scope = [], options = {})
|
16
|
-
key = normalize_flat_keys(locale, key, scope, options[:separator])
|
17
|
-
translations = translations(locale)
|
18
|
-
translations["#{locale}.#{key}".to_sym]
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def translations(locale)
|
24
|
-
Slimmer.cache.fetch("translations/#{locale}", expires_in: Slimmer::CACHE_TTL) do
|
25
|
-
fetch_translations(locale)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def static_locales_url(locale=nil)
|
30
|
-
[static_host, "templates", "locales", locale].compact.join('/')
|
31
|
-
end
|
32
|
-
|
33
|
-
def static_host
|
34
|
-
@static_host ||= Plek.new.find('static')
|
35
|
-
end
|
36
|
-
|
37
|
-
def fetch_translations(locale)
|
38
|
-
url = static_locales_url(locale)
|
39
|
-
json_data = fetch(url)
|
40
|
-
translations = JSON.parse(json_data)
|
41
|
-
flatten_translations(locale, translations, false, false)
|
42
|
-
rescue TemplateNotFoundException
|
43
|
-
{}
|
44
|
-
end
|
45
|
-
|
46
|
-
def fetch(url)
|
47
|
-
HTTPClient.get(url)
|
48
|
-
rescue RestClient::Exception => e
|
49
|
-
raise TemplateNotFoundException, "Unable to fetch: '#{url}' because #{e}", caller
|
50
|
-
rescue Errno::ECONNREFUSED => e
|
51
|
-
raise CouldNotRetrieveTemplate, "Unable to fetch: '#{url}' because #{e}", caller
|
52
|
-
rescue SocketError => e
|
53
|
-
raise CouldNotRetrieveTemplate, "Unable to fetch: '#{url}' because #{e}", caller
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module Slimmer
|
2
|
-
class LocalComponentResolver < ComponentResolver
|
3
|
-
private
|
4
|
-
|
5
|
-
def template_body(template_path)
|
6
|
-
File.read(template_file(template_path))
|
7
|
-
end
|
8
|
-
|
9
|
-
def template_file(template_path)
|
10
|
-
path = template_path.sub(/\.raw(\.html\.erb)?$/, '')
|
11
|
-
|
12
|
-
if defined?(Rails)
|
13
|
-
Rails.root.join("app", "views", "#{path}.raw.html.erb")
|
14
|
-
else
|
15
|
-
"#{path}.raw.html.erb"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
module Slimmer
|
2
|
-
# @api public
|
3
|
-
#
|
4
|
-
# Include this module to avoid loading components over the network
|
5
|
-
# @example
|
6
|
-
# class ApplicationController < ActionController::Base
|
7
|
-
# include Slimmer::LocalGovukComponents
|
8
|
-
# end
|
9
|
-
#
|
10
|
-
# # In your views:
|
11
|
-
#
|
12
|
-
# <%= render partial: 'govuk_component/example_component' %>
|
13
|
-
module LocalGovukComponents
|
14
|
-
def self.included into
|
15
|
-
into.before_action :add_govuk_components
|
16
|
-
end
|
17
|
-
|
18
|
-
# @private
|
19
|
-
def add_govuk_components
|
20
|
-
append_view_path LocalGovukComponents.expiring_resolver_cache.resolver
|
21
|
-
end
|
22
|
-
|
23
|
-
# @private
|
24
|
-
def self.expiring_resolver_cache
|
25
|
-
@expiring_resolver_cache ||= TimedExpirationResolverCache.new
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
# Slimmer::ComponentResolver instantiates a lot of large objects and leaks
|
31
|
-
# memory. This class will cache the resolver so that it doesn't have to
|
32
|
-
# create new ActionView::Template objects for each request. The cache is
|
33
|
-
# timed to allow frontends to pick up changes made to components in `static`.
|
34
|
-
class TimedExpirationResolverCache
|
35
|
-
def initialize
|
36
|
-
@cache_last_reset = Time.now
|
37
|
-
end
|
38
|
-
|
39
|
-
def resolver
|
40
|
-
if (@cache_last_reset + Slimmer::CACHE_TTL) < Time.now
|
41
|
-
@resolver = nil
|
42
|
-
@cache_last_reset = Time.now
|
43
|
-
end
|
44
|
-
|
45
|
-
@resolver ||= Slimmer::LocalComponentResolver.new
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
@@ -1,45 +0,0 @@
|
|
1
|
-
require 'slimmer/govuk_request_id'
|
2
|
-
require 'active_support/core_ext/string/inflections'
|
3
|
-
|
4
|
-
module Slimmer
|
5
|
-
class NetworkComponentResolver < ComponentResolver
|
6
|
-
private
|
7
|
-
|
8
|
-
def template_body(template_path)
|
9
|
-
if test?
|
10
|
-
test_body(template_path)
|
11
|
-
else
|
12
|
-
Slimmer.cache.fetch(template_path, expires_in: Slimmer::CACHE_TTL) do
|
13
|
-
fetch(template_url(template_path))
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def test?
|
19
|
-
defined?(Rails) && Rails.env.test?
|
20
|
-
end
|
21
|
-
|
22
|
-
def fetch(template_url)
|
23
|
-
HTTPClient.get(template_url)
|
24
|
-
rescue RestClient::Exception => e
|
25
|
-
raise TemplateNotFoundException, "Unable to fetch: '#{template_url}' because #{e}", caller
|
26
|
-
rescue Errno::ECONNREFUSED => e
|
27
|
-
raise CouldNotRetrieveTemplate, "Unable to fetch: '#{template_url}' because #{e}", caller
|
28
|
-
rescue SocketError => e
|
29
|
-
raise CouldNotRetrieveTemplate, "Unable to fetch: '#{template_url}' because #{e}", caller
|
30
|
-
end
|
31
|
-
|
32
|
-
def template_url(template_path)
|
33
|
-
path = template_path.sub(/\.raw(\.html\.erb)?$/, '')
|
34
|
-
[static_host, "templates", "#{path}.raw.html.erb"].join('/')
|
35
|
-
end
|
36
|
-
|
37
|
-
def static_host
|
38
|
-
@static_host ||= Plek.new.find('static')
|
39
|
-
end
|
40
|
-
|
41
|
-
def test_body(path)
|
42
|
-
%{<#{TEST_TAG_NAME} data-template="#{path.parameterize}"><%= JSON.dump(local_assigns) %></#{TEST_TAG_NAME}>}
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module Slimmer::Processors
|
2
|
-
class ReportAProblemInserter
|
3
|
-
include ERB::Util
|
4
|
-
|
5
|
-
def initialize(skin, url, headers, wrapper_id)
|
6
|
-
@skin = skin
|
7
|
-
@request_url = url
|
8
|
-
@headers = headers
|
9
|
-
@wrapper_id = wrapper_id
|
10
|
-
end
|
11
|
-
|
12
|
-
def filter(content_document, page_template)
|
13
|
-
if enabled? && container = page_template.at_css('#' + @wrapper_id)
|
14
|
-
container.add_child(report_a_problem_block)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def report_a_problem_block
|
19
|
-
request_url = @request_url
|
20
|
-
source = @headers[Slimmer::Headers::APPLICATION_NAME_HEADER]
|
21
|
-
page_owner = @headers[Slimmer::Headers::PAGE_OWNER_HEADER]
|
22
|
-
report_a_problem_template = @skin.template('report_a_problem.raw')
|
23
|
-
html = ERB.new(report_a_problem_template).result(binding)
|
24
|
-
Nokogiri::HTML.fragment(html)
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
def enabled?
|
29
|
-
@headers[Slimmer::Headers::REPORT_A_PROBLEM_FORM] != 'false'
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
require 'webmock'
|
2
|
-
|
3
|
-
module Slimmer
|
4
|
-
module TestHelpers
|
5
|
-
module GovukComponents
|
6
|
-
def stub_shared_component_locales
|
7
|
-
stub_request(:get, /https?:\/\/\S+.gov.uk\/templates\/locales\/.+/).
|
8
|
-
to_return(status: 400, headers: {})
|
9
|
-
stub_request(:get, /https?:\/\/\S+.gov.uk\/templates\/locales/).
|
10
|
-
to_return(status: 200, body: '{}', headers: {})
|
11
|
-
end
|
12
|
-
|
13
|
-
def shared_component_selector(name)
|
14
|
-
"#{Slimmer::ComponentResolver::TEST_TAG_NAME}[data-template='govuk_component-#{name}']"
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|