nexmo-oas-renderer 0.11.2 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +135 -0
- data/.travis.yml +1 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +91 -71
- data/Rakefile +2 -2
- data/bin/console +3 -3
- data/lib/nexmo/oas/engine.rb +2 -0
- data/lib/nexmo/oas/renderer.rb +5 -5
- data/lib/nexmo/oas/renderer/app.rb +71 -46
- data/lib/nexmo/oas/renderer/config.ru +4 -2
- data/lib/nexmo/oas/renderer/decorators/response_parser_decorator.rb +18 -18
- data/lib/nexmo/oas/renderer/helpers/navigation.rb +2 -2
- data/lib/nexmo/oas/renderer/helpers/render.rb +2 -1
- data/lib/nexmo/oas/renderer/helpers/summary.rb +4 -1
- data/lib/nexmo/oas/renderer/helpers/url.rb +2 -0
- data/lib/nexmo/oas/renderer/presenters/api_specification.rb +12 -3
- data/lib/nexmo/oas/renderer/presenters/endpoint.rb +2 -0
- data/lib/nexmo/oas/renderer/presenters/groups.rb +4 -0
- data/lib/nexmo/oas/renderer/presenters/navigation.rb +2 -0
- data/lib/nexmo/oas/renderer/presenters/open_api_specification.rb +5 -2
- data/lib/nexmo/oas/renderer/presenters/request_body_raw.rb +141 -0
- data/lib/nexmo/oas/renderer/presenters/response_format.rb +4 -2
- data/lib/nexmo/oas/renderer/presenters/response_tab/link.rb +3 -0
- data/lib/nexmo/oas/renderer/presenters/response_tab/panel.rb +8 -5
- data/lib/nexmo/oas/renderer/presenters/response_tabs.rb +6 -2
- data/lib/nexmo/oas/renderer/presenters/versions.rb +11 -10
- data/lib/nexmo/oas/renderer/public/assets/javascripts/components/format.js +12 -7
- data/lib/nexmo/oas/renderer/public/assets/javascripts/nexmo-oas-renderer.js +61 -27
- data/lib/nexmo/oas/renderer/public/assets/javascripts/popper.min.js +5 -0
- data/lib/nexmo/oas/renderer/public/assets/javascripts/tooltip.min.js +5 -0
- data/lib/nexmo/oas/renderer/public/assets/javascripts/volta.accordion.js +301 -243
- data/lib/nexmo/oas/renderer/public/assets/javascripts/volta.tooltip.js +76 -0
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/nexmo-oas-renderer.css +255 -823
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/nexmo-oas-renderer.css.map +2 -2
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/api.scss +287 -90
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/style.scss +2 -6
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/themes/dark.scss +89 -0
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/themes/light.scss +68 -0
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/volta-prism.min.css +1 -1
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/volta.min.css +1 -1
- data/lib/nexmo/oas/renderer/services/oas_parser.rb +2 -0
- data/lib/nexmo/oas/renderer/services/open_api_definition_resolver.rb +3 -1
- data/lib/nexmo/oas/renderer/version.rb +3 -1
- data/lib/nexmo/oas/renderer/views/layouts/_head.erb +1 -0
- data/lib/nexmo/oas/renderer/views/layouts/_javascripts.erb +5 -4
- data/lib/nexmo/oas/renderer/views/layouts/open_api.erb +3 -1
- data/lib/nexmo/oas/renderer/views/open_api/_auth.erb +74 -0
- data/lib/nexmo/oas/renderer/views/open_api/_available_endpoints.erb +25 -0
- data/lib/nexmo/oas/renderer/views/open_api/_callback_endpoint.erb +18 -27
- data/lib/nexmo/oas/renderer/views/open_api/_callbacks.erb +5 -0
- data/lib/nexmo/oas/renderer/views/open_api/_endpoint.erb +39 -124
- data/lib/nexmo/oas/renderer/views/open_api/_header.erb +71 -0
- data/lib/nexmo/oas/renderer/views/open_api/_model.erb +31 -26
- data/lib/nexmo/oas/renderer/views/open_api/_navigation.erb +54 -78
- data/lib/nexmo/oas/renderer/views/open_api/_parameter_groups.erb +2 -5
- data/lib/nexmo/oas/renderer/views/open_api/_parameters.erb +98 -169
- data/lib/nexmo/oas/renderer/views/open_api/_request_json.erb +4 -0
- data/lib/nexmo/oas/renderer/views/open_api/_request_one_of.erb +70 -0
- data/lib/nexmo/oas/renderer/views/open_api/_request_single.erb +53 -0
- data/lib/nexmo/oas/renderer/views/open_api/_requests.erb +22 -0
- data/lib/nexmo/oas/renderer/views/open_api/_response_description_parameters.erb +88 -90
- data/lib/nexmo/oas/renderer/views/open_api/_response_descriptions.erb +32 -12
- data/lib/nexmo/oas/renderer/views/open_api/_response_fields.erb +1 -16
- data/lib/nexmo/oas/renderer/views/open_api/_response_tabs.erb +2 -2
- data/lib/nexmo/oas/renderer/views/open_api/_responses.erb +51 -0
- data/lib/nexmo/oas/renderer/views/open_api/_tabbed_parameters.erb +15 -4
- data/lib/nexmo/oas/renderer/views/open_api/_tabbed_single_parameter.erb +56 -0
- data/lib/nexmo/oas/renderer/views/open_api/_webhooks.erb +30 -0
- data/lib/nexmo/oas/renderer/views/open_api/show.erb +10 -115
- data/nexmo-oas-renderer.gemspec +26 -26
- metadata +59 -48
- data/lib/nexmo/oas/renderer/public/assets/javascripts/components/scroll.js +0 -21
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/core.scss +0 -137
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/navigation.scss +0 -102
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/nexmo.scss +0 -61
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/syntax.scss +0 -63
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/typography.scss +0 -248
- data/lib/nexmo/oas/renderer/public/assets/stylesheets/sass/volta-templates.scss +0 -119
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
require_relative './endpoint'
|
3
5
|
require_relative './response_format'
|
@@ -9,7 +11,6 @@ module Nexmo
|
|
9
11
|
module OAS
|
10
12
|
module Renderer
|
11
13
|
module Presenters
|
12
|
-
|
13
14
|
class OpenApiSpecification
|
14
15
|
extend Forwardable
|
15
16
|
|
@@ -30,9 +31,11 @@ module Nexmo
|
|
30
31
|
end
|
31
32
|
|
32
33
|
def definition_errors
|
34
|
+
return unless errors?
|
35
|
+
|
33
36
|
@definition_errors ||= Nexmo::Markdown::Renderer.new.call(
|
34
37
|
File.read("#{API.oas_path}/../../errors/#{@definition_name}.md")
|
35
|
-
)
|
38
|
+
)
|
36
39
|
end
|
37
40
|
|
38
41
|
def definition
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nexmo
|
4
|
+
module OAS
|
5
|
+
module Renderer
|
6
|
+
module Presenters
|
7
|
+
class RequestBodyRaw
|
8
|
+
def initialize(parameters, options = {}, endpoint = nil)
|
9
|
+
@parameters = parameters
|
10
|
+
@options = options
|
11
|
+
@endpoint = endpoint
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_format(format)
|
15
|
+
return to_urlencoded if format == 'application/x-www-form-urlencoded'
|
16
|
+
|
17
|
+
to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_urlencoded
|
21
|
+
example = ''
|
22
|
+
body = URI.encode_www_form(generate_request)
|
23
|
+
if @endpoint
|
24
|
+
servers = @endpoint.path.servers || @endpoint.definition.servers
|
25
|
+
path = @endpoint.path.path.gsub(/\{(.+?)\}/, ':\1')
|
26
|
+
uri = URI("#{servers[0]['url']}#{path}")
|
27
|
+
example += "#{@endpoint.method.upcase} #{uri.path} HTTP/1.1\n"
|
28
|
+
example += "Host: #{uri.host} \n"
|
29
|
+
example += "Content-Type: application/x-www-form-urlencoded\n"
|
30
|
+
example += "Content-Length: #{body.length}\n"
|
31
|
+
example += "\n"
|
32
|
+
end
|
33
|
+
example += body
|
34
|
+
example
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_json(*_args)
|
38
|
+
JSON.pretty_generate(generate_request)
|
39
|
+
end
|
40
|
+
|
41
|
+
def generate_request(parameters = nil, options = nil)
|
42
|
+
parameters ||= @parameters
|
43
|
+
options ||= @options
|
44
|
+
output = {}
|
45
|
+
|
46
|
+
parameters.each do |parameter|
|
47
|
+
next if options['required_only'] && optional?(parameter, options['required'])
|
48
|
+
|
49
|
+
parameter_name = name(parameter)
|
50
|
+
param = parameter
|
51
|
+
|
52
|
+
# This is all required to handle an edge case where parameter.name is an OasParser::Property
|
53
|
+
# Which happens when you use a oneOf inside items in a property.
|
54
|
+
# I believe this is a bug, but it's a BC break in the parser
|
55
|
+
|
56
|
+
if parameter_name.instance_of?(OasParser::Property)
|
57
|
+
parameter_name = parameter.owner.name
|
58
|
+
param = OasParser::Parameter.new(parameter_name, parameter.schema)
|
59
|
+
end
|
60
|
+
|
61
|
+
if param.raw['items'] && param.raw['example']
|
62
|
+
output[parameter_name] = param.raw['example']
|
63
|
+
elsif param.raw['items'] && param.raw['items']['oneOf']
|
64
|
+
param = param.raw['items']['oneOf'][0]
|
65
|
+
output[parameter_name] = [generate_request(properties(param).map(&:name))]
|
66
|
+
elsif collection?(param) && properties?(param)
|
67
|
+
nested_output = generate_request(properties(param))
|
68
|
+
next unless nested_output.keys.length.positive?
|
69
|
+
|
70
|
+
nested_output = [nested_output] if param.array?
|
71
|
+
output[parameter_name] = nested_output
|
72
|
+
else
|
73
|
+
ex = example(param)
|
74
|
+
next unless ex
|
75
|
+
|
76
|
+
if ex.is_a?(String)
|
77
|
+
# Remove line breaks
|
78
|
+
ex = ex.gsub('<br />', ' ')
|
79
|
+
end
|
80
|
+
output[parameter_name] = ex
|
81
|
+
end
|
82
|
+
end
|
83
|
+
output
|
84
|
+
end
|
85
|
+
|
86
|
+
def items(parameter)
|
87
|
+
return parameter['items'] unless parameter.respond_to?(:items)
|
88
|
+
|
89
|
+
parameter.items
|
90
|
+
end
|
91
|
+
|
92
|
+
def example(parameter)
|
93
|
+
return parameter['example'] unless parameter.respond_to?(:example)
|
94
|
+
|
95
|
+
parameter.example
|
96
|
+
end
|
97
|
+
|
98
|
+
def name(parameter)
|
99
|
+
return parameter['name'] unless parameter.respond_to?(:name)
|
100
|
+
|
101
|
+
parameter.name
|
102
|
+
end
|
103
|
+
|
104
|
+
def properties(parameter)
|
105
|
+
return parameter['properties'] unless parameter.respond_to?(:properties)
|
106
|
+
|
107
|
+
parameter.properties
|
108
|
+
end
|
109
|
+
|
110
|
+
def array?(parameter)
|
111
|
+
return parameter['items'] unless parameter.respond_to?(:array?)
|
112
|
+
|
113
|
+
parameter.array?
|
114
|
+
end
|
115
|
+
|
116
|
+
def collection?(parameter)
|
117
|
+
return parameter['properties'] unless parameter.respond_to?(:collection?)
|
118
|
+
|
119
|
+
parameter.collection?
|
120
|
+
end
|
121
|
+
|
122
|
+
def properties?(parameter)
|
123
|
+
return parameter['properties'] unless parameter.respond_to?(:collection?)
|
124
|
+
|
125
|
+
parameter.properties.size.positive?
|
126
|
+
end
|
127
|
+
|
128
|
+
def optional?(parameter, allow_list)
|
129
|
+
return false if allow_list&.include?(name(parameter))
|
130
|
+
|
131
|
+
return false unless parameter.respond_to?(:required)
|
132
|
+
|
133
|
+
return false unless parameter.schema
|
134
|
+
|
135
|
+
!parameter.required
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Nexmo
|
2
4
|
module OAS
|
3
5
|
module Renderer
|
@@ -7,7 +9,7 @@ module Nexmo
|
|
7
9
|
'application/json' => 'JSON',
|
8
10
|
'application/xml' => 'XML',
|
9
11
|
'text/xml' => 'XML',
|
10
|
-
}
|
12
|
+
}.freeze
|
11
13
|
|
12
14
|
def self.to_dropdown(formats)
|
13
15
|
MAPPINGS.slice(*formats)
|
@@ -18,7 +20,7 @@ module Nexmo
|
|
18
20
|
end
|
19
21
|
|
20
22
|
def extract
|
21
|
-
@
|
23
|
+
@extract ||= @responses.flat_map(&:formats).uniq
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Nexmo
|
2
4
|
module OAS
|
3
5
|
module Renderer
|
@@ -6,12 +8,13 @@ module Nexmo
|
|
6
8
|
class Panel
|
7
9
|
attr_reader :index
|
8
10
|
|
9
|
-
def initialize(schema:, index:, format:, content:, endpoint:)
|
11
|
+
def initialize(schema:, index:, format:, content:, endpoint:, theme_light: nil) # rubocop:disable Metrics/ParameterLists
|
10
12
|
@schema = schema
|
11
13
|
@index = index
|
12
14
|
@format = format
|
13
15
|
@content = content
|
14
16
|
@endpoint = endpoint
|
17
|
+
@theme_light = theme_light
|
15
18
|
end
|
16
19
|
|
17
20
|
def css_classes
|
@@ -22,15 +25,15 @@ module Nexmo
|
|
22
25
|
|
23
26
|
def content
|
24
27
|
if @content == :responses
|
25
|
-
Nexmo::OAS::Renderer::ResponseParserDecorator
|
26
|
-
new(@schema)
|
27
|
-
html(@format, xml_options: @schema['xml'])
|
28
|
+
Nexmo::OAS::Renderer::ResponseParserDecorator
|
29
|
+
.new(@schema)
|
30
|
+
.html(@format, xml_options: @schema['xml'], theme_light: @theme_light)
|
28
31
|
else
|
29
32
|
[:'open_api/_response_fields', locals: {
|
30
33
|
schema: @schema,
|
31
34
|
index: @index,
|
32
35
|
format: @format,
|
33
|
-
endpoint: @endpoint
|
36
|
+
endpoint: @endpoint,
|
34
37
|
}]
|
35
38
|
end
|
36
39
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative './response_tab/link'
|
2
4
|
require_relative './response_tab/panel'
|
3
5
|
|
@@ -8,11 +10,12 @@ module Nexmo
|
|
8
10
|
class ResponseTabs
|
9
11
|
attr_reader :format
|
10
12
|
|
11
|
-
def initialize(format, response, content, endpoint)
|
13
|
+
def initialize(format, response, content, endpoint, theme_light: nil)
|
12
14
|
@format = format
|
13
15
|
@response = response
|
14
16
|
@content = content
|
15
17
|
@endpoint = endpoint
|
18
|
+
@theme_light = theme_light
|
16
19
|
end
|
17
20
|
|
18
21
|
def tab_links
|
@@ -33,7 +36,8 @@ module Nexmo
|
|
33
36
|
index: index,
|
34
37
|
format: @format,
|
35
38
|
content: @content,
|
36
|
-
endpoint: @endpoint
|
39
|
+
endpoint: @endpoint,
|
40
|
+
theme_light: @theme_light
|
37
41
|
)
|
38
42
|
end
|
39
43
|
end
|
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Nexmo
|
2
4
|
module OAS
|
3
5
|
module Renderer
|
4
6
|
module Presenters
|
5
|
-
|
6
7
|
class Versions
|
7
8
|
def initialize(definition_name)
|
8
9
|
@definition_name = definition_name
|
@@ -22,28 +23,28 @@ module Nexmo
|
|
22
23
|
def available_versions
|
23
24
|
@available_versions ||= begin
|
24
25
|
matches = definitions.select do |definition|
|
25
|
-
definition.
|
26
|
+
definition.match(/^#{base_name}(\.v\d+)?$/) && !definition.include?("#{base_name}/")
|
26
27
|
end
|
27
28
|
|
28
|
-
matches.map do |definition|
|
29
|
-
|
30
|
-
|
31
|
-
next { 'version' => '1', 'name' => name } unless m
|
32
|
-
{ 'version' => m[1], 'name' => name }
|
29
|
+
matches = matches.map do |definition|
|
30
|
+
m = /\.v(\d+)/.match(definition)
|
31
|
+
next { 'version' => '1', 'name' => definition } unless m
|
33
32
|
|
34
|
-
|
33
|
+
{ 'version' => m[1], 'name' => definition }
|
34
|
+
end
|
35
|
+
|
36
|
+
matches.sort_by { |v| v['version'] }
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
38
40
|
def definitions
|
39
41
|
@definitions ||= begin
|
40
42
|
Dir.glob("#{API.oas_path}/**/*.yml").map do |file|
|
41
|
-
|
43
|
+
file.sub("#{API.oas_path}/", '').chomp('.yml')
|
42
44
|
end
|
43
45
|
end
|
44
46
|
end
|
45
47
|
end
|
46
|
-
|
47
48
|
end
|
48
49
|
end
|
49
50
|
end
|
@@ -1,11 +1,14 @@
|
|
1
1
|
class Format {
|
2
2
|
constructor() {
|
3
|
-
this.
|
4
|
-
|
5
|
-
|
6
|
-
this.
|
7
|
-
|
8
|
-
|
3
|
+
this.formatSelectors = document.getElementsByClassName('js-format-selector');
|
4
|
+
|
5
|
+
if (this.formatSelectors.length) {
|
6
|
+
this.formatChanged(this.formatSelectors[0].value, false)
|
7
|
+
for (let selector of this.formatSelectors) {
|
8
|
+
selector.addEventListener('change', (event) => {
|
9
|
+
this.formatChanged(event.target.value)
|
10
|
+
})
|
11
|
+
}
|
9
12
|
|
10
13
|
this.restoreFormat()
|
11
14
|
}
|
@@ -34,7 +37,9 @@ class Format {
|
|
34
37
|
if (window.localStorage) {
|
35
38
|
const format = window.localStorage.getItem('format')
|
36
39
|
if (format) {
|
37
|
-
this.
|
40
|
+
for (let selector of this.formatSelectors) {
|
41
|
+
selector.value = format
|
42
|
+
}
|
38
43
|
this.formatChanged(format, false)
|
39
44
|
}
|
40
45
|
}
|
@@ -1,31 +1,65 @@
|
|
1
|
-
document.
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
var newText;
|
10
|
-
if (text.indexOf('View') != -1) {
|
11
|
-
newText = text.replace("View", "Hide");
|
12
|
-
} else {
|
13
|
-
newText = text.replace("Hide", "View");
|
14
|
-
}
|
15
|
-
event.target.textContent = newText;
|
16
|
-
});
|
17
|
-
});
|
1
|
+
if(document.querySelector(".oas-wrapper")){
|
2
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
3
|
+
var queryString = new URLSearchParams(window.location.search);
|
4
|
+
if (!queryString.get('theme')) {
|
5
|
+
queryString.set("theme", "dark");
|
6
|
+
window.location.search = queryString.toString();
|
7
|
+
}
|
8
|
+
}
|
18
9
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
10
|
+
document.addEventListener("DOMContentLoaded", function() {
|
11
|
+
// Handle people clicking on oneOf tabs by changing every one on the page
|
12
|
+
var oneOfTabs = document.querySelectorAll('[data-tab-link]');
|
13
|
+
Array.from(oneOfTabs).forEach(function (element) {
|
14
|
+
element.addEventListener('click', function (event) {
|
15
|
+
var link = event.target.getAttribute('data-tab-link');
|
16
|
+
var matchingTabs = document.querySelectorAll('[data-tab-link="' + link + '"]');
|
17
|
+
Array.from(matchingTabs).forEach(function (element) {
|
18
|
+
element.dispatchEvent(new Event('toggle'));
|
28
19
|
});
|
20
|
+
});
|
29
21
|
});
|
30
22
|
|
31
|
-
|
23
|
+
var toggleTopNav = function(event, closeOnly) {
|
24
|
+
|
25
|
+
if (event) {
|
26
|
+
if (!Array.from(event.target.classList).includes("oas-trigger")) {
|
27
|
+
return;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
var c = document.querySelector('.oas-trigger-content');
|
32
|
+
if (closeOnly === true) {
|
33
|
+
c.style.display = "none";
|
34
|
+
return;
|
35
|
+
}
|
36
|
+
c.style.display = c.style.display == 'block' ? 'none' : "block";
|
37
|
+
};
|
38
|
+
|
39
|
+
if (document.querySelector('.oas-trigger')) {
|
40
|
+
document.querySelector('.oas-trigger').addEventListener("click", toggleTopNav);
|
41
|
+
}
|
42
|
+
|
43
|
+
document.querySelectorAll('a').forEach((element) => {
|
44
|
+
var href = element.getAttribute("href");
|
45
|
+
if (!href){ return; }
|
46
|
+
if (href.slice(0,1) !== "#"){ return; }
|
47
|
+
element.addEventListener('click', function (event) {
|
48
|
+
event.preventDefault();
|
49
|
+
|
50
|
+
toggleTopNav(null, true);
|
51
|
+
|
52
|
+
var to = document.querySelector(event.target.hash)
|
53
|
+
history.pushState({}, '', event.target.href);
|
54
|
+
window.scrollTo({
|
55
|
+
top: (window.scrollY + to.getBoundingClientRect().y) - 70,
|
56
|
+
left: 0,
|
57
|
+
})
|
58
|
+
|
59
|
+
|
60
|
+
});
|
61
|
+
});
|
62
|
+
|
63
|
+
});
|
64
|
+
}
|
65
|
+
|