gherkin 7.0.4 → 8.0.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 +4 -4
- data/bin/gherkin +34 -12
- data/lib/gherkin.rb +42 -0
- data/lib/gherkin/ast_builder.rb +260 -0
- data/lib/gherkin/ast_node.rb +30 -0
- data/lib/gherkin/dialect.rb +2 -12
- data/lib/gherkin/errors.rb +45 -0
- data/lib/gherkin/gherkin-languages.json +3520 -0
- data/lib/gherkin/gherkin_line.rb +97 -0
- data/lib/gherkin/parser.rb +3429 -0
- data/lib/gherkin/pickles/compiler.rb +233 -0
- data/lib/gherkin/stream/parser_message_stream.rb +93 -0
- data/lib/gherkin/stream/protobuf_message_stream.rb +21 -0
- data/lib/gherkin/stream/subprocess_message_stream.rb +26 -0
- data/lib/gherkin/token.rb +18 -0
- data/lib/gherkin/token_formatter_builder.rb +39 -0
- data/lib/gherkin/token_matcher.rb +169 -0
- data/lib/gherkin/token_scanner.rb +40 -0
- data/spec/gherkin/gherkin_spec.rb +36 -345
- data/spec/gherkin/stream/subprocess_message_stream_spec.rb +18 -0
- metadata +28 -53
- data/executables/gherkin-darwin-386 +0 -0
- data/executables/gherkin-darwin-amd64 +0 -0
- data/executables/gherkin-freebsd-386 +0 -0
- data/executables/gherkin-freebsd-amd64 +0 -0
- data/executables/gherkin-freebsd-arm +0 -0
- data/executables/gherkin-linux-386 +0 -0
- data/executables/gherkin-linux-amd64 +0 -0
- data/executables/gherkin-linux-arm +0 -0
- data/executables/gherkin-linux-mips +0 -0
- data/executables/gherkin-linux-mips64 +0 -0
- data/executables/gherkin-linux-mips64le +0 -0
- data/executables/gherkin-linux-mipsle +0 -0
- data/executables/gherkin-linux-s390x +0 -0
- data/executables/gherkin-netbsd-386 +0 -0
- data/executables/gherkin-netbsd-amd64 +0 -0
- data/executables/gherkin-netbsd-arm +0 -0
- data/executables/gherkin-openbsd-386 +0 -0
- data/executables/gherkin-openbsd-amd64 +0 -0
- data/executables/gherkin-windows-386.exe +0 -0
- data/executables/gherkin-windows-amd64.exe +0 -0
- data/lib/gherkin/exe_file_path.rb +0 -3
- data/lib/gherkin/gherkin.rb +0 -72
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 03d427e820bf0e7c744cbe67f7f9173ade3a528dd7a3f02c94ef6b261d54fe7b
|
4
|
+
data.tar.gz: 01d2c0fca9b02b4a0c6c480ab47c10178210814c86e7dd6f4ffcdde9540dcfda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c638788e4c6926086d5271cf414665d3fdaebf464643283238ca6f86cb000d1650c69017c5bc11260c2770a093b0c9538ab2ab0df73e44b2d9ef1c7e4f208475
|
7
|
+
data.tar.gz: 4960487bbdde80f6d74df8e81fd4d3983a1fe2c998ea5d9842cfc8db9ab4607cf0c4909f5c89a2b1f19a0e8614eaabd0b673ed27d79488651da1caf51606cecb
|
data/bin/gherkin
CHANGED
@@ -4,12 +4,16 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__),"../lib"))
|
|
4
4
|
|
5
5
|
require 'optparse'
|
6
6
|
require 'json'
|
7
|
-
require '
|
7
|
+
require 'cucumber/messages'
|
8
|
+
require 'gherkin'
|
9
|
+
require 'gherkin/stream/subprocess_message_stream'
|
10
|
+
require 'gherkin/stream/protobuf_message_stream'
|
8
11
|
|
9
12
|
options = {
|
10
13
|
include_source: true,
|
11
14
|
include_gherkin_document: true,
|
12
|
-
include_pickles: true
|
15
|
+
include_pickles: true,
|
16
|
+
json: false
|
13
17
|
}
|
14
18
|
|
15
19
|
OptionParser.new do |opts|
|
@@ -22,14 +26,21 @@ OptionParser.new do |opts|
|
|
22
26
|
opts.on("--[no-]pickles", "Don't print pickle messages") do |v|
|
23
27
|
options[:include_pickles] = v
|
24
28
|
end
|
29
|
+
opts.on("--json", "Print messages as JSON") do |v|
|
30
|
+
options[:json] = v
|
31
|
+
end
|
25
32
|
end.parse!
|
26
33
|
|
27
|
-
def
|
34
|
+
def process_messages(messages, options)
|
28
35
|
messages.each do |message|
|
29
|
-
json
|
30
|
-
|
31
|
-
|
32
|
-
|
36
|
+
if options[:json]
|
37
|
+
json = message.class.encode_json(message)
|
38
|
+
ob = JSON.parse(json)
|
39
|
+
remove_empties(ob)
|
40
|
+
puts JSON.generate(ob)
|
41
|
+
else
|
42
|
+
message.write_delimited_to(STDOUT)
|
43
|
+
end
|
33
44
|
end
|
34
45
|
end
|
35
46
|
|
@@ -49,12 +60,23 @@ def remove_empties(ob)
|
|
49
60
|
end
|
50
61
|
end
|
51
62
|
|
52
|
-
|
63
|
+
gherkin_executable = ENV['GHERKIN_EXECUTABLE']
|
64
|
+
if ARGV.empty?
|
65
|
+
# Read protobuf from STDIN
|
66
|
+
messages = Gherkin::Stream::ProtobufMessageStream.new(STDIN).messages
|
67
|
+
elsif gherkin_executable
|
53
68
|
# Read protobuf from STDIN
|
54
|
-
Gherkin::
|
55
|
-
|
56
|
-
|
69
|
+
messages = Gherkin::Stream::SubprocessMessageStream.new(
|
70
|
+
gherkin_executable,
|
71
|
+
ARGV,
|
72
|
+
options[:include_source],
|
73
|
+
options[:include_gherkin_document],
|
74
|
+
options[:include_pickles]
|
75
|
+
).messages
|
76
|
+
else
|
77
|
+
messages = Gherkin.from_paths(
|
57
78
|
ARGV,
|
58
79
|
options
|
59
80
|
)
|
60
|
-
|
81
|
+
end
|
82
|
+
process_messages(messages, options)
|
data/lib/gherkin.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'gherkin/stream/parser_message_stream'
|
2
|
+
|
3
|
+
module Gherkin
|
4
|
+
DEFAULT_OPTIONS = {
|
5
|
+
include_source: true,
|
6
|
+
include_gherkin_document: true,
|
7
|
+
include_pickles: true
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
def self.from_paths(paths, options={})
|
11
|
+
Stream::ParserMessageStream.new(
|
12
|
+
paths,
|
13
|
+
[],
|
14
|
+
options
|
15
|
+
).messages
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_sources(sources, options={})
|
19
|
+
Stream::ParserMessageStream.new(
|
20
|
+
[],
|
21
|
+
sources,
|
22
|
+
options
|
23
|
+
).messages
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.from_source(uri, data, options={})
|
27
|
+
from_sources([encode_source_message(uri, data)], options)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def self.encode_source_message(uri, data)
|
33
|
+
Cucumber::Messages::Source.new({
|
34
|
+
uri: uri,
|
35
|
+
data: data,
|
36
|
+
media: Cucumber::Messages::Media.new({
|
37
|
+
encoding: :UTF8,
|
38
|
+
content_type: 'text/x.cucumber.gherkin+plain'
|
39
|
+
})
|
40
|
+
})
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'cucumber/messages'
|
2
|
+
require 'gherkin/ast_node'
|
3
|
+
|
4
|
+
module Gherkin
|
5
|
+
class AstBuilder
|
6
|
+
def initialize
|
7
|
+
reset
|
8
|
+
end
|
9
|
+
|
10
|
+
def reset
|
11
|
+
@stack = [AstNode.new(:None)]
|
12
|
+
@comments = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def start_rule(rule_type)
|
16
|
+
@stack.push AstNode.new(rule_type)
|
17
|
+
end
|
18
|
+
|
19
|
+
def end_rule(rule_type)
|
20
|
+
node = @stack.pop
|
21
|
+
current_node.add(node.rule_type, transform_node(node))
|
22
|
+
end
|
23
|
+
|
24
|
+
def build(token)
|
25
|
+
if token.matched_type == :Comment
|
26
|
+
@comments.push(Cucumber::Messages::GherkinDocument::Comment.new(
|
27
|
+
location: get_location(token, 0),
|
28
|
+
text: token.matched_text
|
29
|
+
))
|
30
|
+
else
|
31
|
+
current_node.add(token.matched_type, token)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_result
|
36
|
+
current_node.get_single(:GherkinDocument)
|
37
|
+
end
|
38
|
+
|
39
|
+
def current_node
|
40
|
+
@stack.last
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_location(token, column)
|
44
|
+
column = column == 0 ? token.location[:column] : column
|
45
|
+
Cucumber::Messages::Location.new(
|
46
|
+
line: token.location[:line],
|
47
|
+
column: column
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_tags(node)
|
52
|
+
tags = []
|
53
|
+
tags_node = node.get_single(:Tags)
|
54
|
+
return tags unless tags_node
|
55
|
+
|
56
|
+
tags_node.get_tokens(:TagLine).each do |token|
|
57
|
+
token.matched_items.each do |tag_item|
|
58
|
+
tags.push(Cucumber::Messages::GherkinDocument::Feature::Tag.new(
|
59
|
+
location: get_location(token, tag_item.column),
|
60
|
+
name: tag_item.text
|
61
|
+
))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
tags
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_table_rows(node)
|
69
|
+
rows = node.get_tokens(:TableRow).map do |token|
|
70
|
+
Cucumber::Messages::GherkinDocument::Feature::TableRow.new(
|
71
|
+
location: get_location(token, 0),
|
72
|
+
cells: get_cells(token)
|
73
|
+
)
|
74
|
+
end
|
75
|
+
ensure_cell_count(rows)
|
76
|
+
rows
|
77
|
+
end
|
78
|
+
|
79
|
+
def ensure_cell_count(rows)
|
80
|
+
return if rows.empty?
|
81
|
+
cell_count = rows[0].cells.length
|
82
|
+
rows.each do |row|
|
83
|
+
if row.cells.length != cell_count
|
84
|
+
location = {line: row.location.line, column: row.location.column}
|
85
|
+
raise AstBuilderException.new("inconsistent cell count within the table", location)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_cells(table_row_token)
|
91
|
+
table_row_token.matched_items.map do |cell_item|
|
92
|
+
Cucumber::Messages::GherkinDocument::Feature::TableRow::TableCell.new(
|
93
|
+
location: get_location(table_row_token, cell_item.column),
|
94
|
+
value: cell_item.text
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_description(node)
|
100
|
+
node.get_single(:Description)
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_steps(node)
|
104
|
+
node.get_items(:Step)
|
105
|
+
end
|
106
|
+
|
107
|
+
def transform_node(node)
|
108
|
+
case node.rule_type
|
109
|
+
when :Step
|
110
|
+
step_line = node.get_token(:StepLine)
|
111
|
+
data_table = node.get_single(:DataTable)
|
112
|
+
doc_string = node.get_single(:DocString)
|
113
|
+
|
114
|
+
props = {
|
115
|
+
location: get_location(step_line, 0),
|
116
|
+
keyword: step_line.matched_keyword,
|
117
|
+
text: step_line.matched_text
|
118
|
+
}
|
119
|
+
props[:data_table] = data_table if data_table
|
120
|
+
props[:doc_string] = doc_string if doc_string
|
121
|
+
|
122
|
+
Cucumber::Messages::GherkinDocument::Feature::Step.new(props)
|
123
|
+
when :DocString
|
124
|
+
separator_token = node.get_tokens(:DocStringSeparator)[0]
|
125
|
+
content_type = separator_token.matched_text == '' ? nil : separator_token.matched_text
|
126
|
+
line_tokens = node.get_tokens(:Other)
|
127
|
+
content = line_tokens.map { |t| t.matched_text }.join("\n")
|
128
|
+
|
129
|
+
props = {
|
130
|
+
location: get_location(separator_token, 0),
|
131
|
+
content: content,
|
132
|
+
delimiter: separator_token.matched_keyword
|
133
|
+
}
|
134
|
+
props[:content_type] = content_type if content_type
|
135
|
+
Cucumber::Messages::GherkinDocument::Feature::Step::DocString.new(props)
|
136
|
+
when :DataTable
|
137
|
+
rows = get_table_rows(node)
|
138
|
+
Cucumber::Messages::GherkinDocument::Feature::Step::DataTable.new(
|
139
|
+
location: rows[0].location,
|
140
|
+
rows: rows,
|
141
|
+
)
|
142
|
+
when :Background
|
143
|
+
background_line = node.get_token(:BackgroundLine)
|
144
|
+
description = get_description(node)
|
145
|
+
steps = get_steps(node)
|
146
|
+
|
147
|
+
props = {
|
148
|
+
location: get_location(background_line, 0),
|
149
|
+
keyword: background_line.matched_keyword,
|
150
|
+
name: background_line.matched_text,
|
151
|
+
steps: steps
|
152
|
+
}
|
153
|
+
props[:description] = description if description
|
154
|
+
Cucumber::Messages::GherkinDocument::Feature::Background.new(props)
|
155
|
+
when :ScenarioDefinition
|
156
|
+
tags = get_tags(node)
|
157
|
+
scenario_node = node.get_single(:Scenario)
|
158
|
+
scenario_line = scenario_node.get_token(:ScenarioLine)
|
159
|
+
description = get_description(scenario_node)
|
160
|
+
steps = get_steps(scenario_node)
|
161
|
+
examples = scenario_node.get_items(:ExamplesDefinition)
|
162
|
+
props = {
|
163
|
+
tags: tags,
|
164
|
+
location: get_location(scenario_line, 0),
|
165
|
+
keyword: scenario_line.matched_keyword,
|
166
|
+
name: scenario_line.matched_text,
|
167
|
+
steps: steps,
|
168
|
+
examples: examples
|
169
|
+
}
|
170
|
+
props[:description] = description if description
|
171
|
+
Cucumber::Messages::GherkinDocument::Feature::Scenario.new(props)
|
172
|
+
when :ExamplesDefinition
|
173
|
+
tags = get_tags(node)
|
174
|
+
examples_node = node.get_single(:Examples)
|
175
|
+
examples_line = examples_node.get_token(:ExamplesLine)
|
176
|
+
description = get_description(examples_node)
|
177
|
+
rows = examples_node.get_single(:ExamplesTable)
|
178
|
+
|
179
|
+
table_header = rows.nil? ? nil : rows.first
|
180
|
+
table_body = rows.nil? ? nil : rows[1..-1]
|
181
|
+
|
182
|
+
props = {
|
183
|
+
tags: tags,
|
184
|
+
location: get_location(examples_line, 0),
|
185
|
+
keyword: examples_line.matched_keyword,
|
186
|
+
name: examples_line.matched_text,
|
187
|
+
}
|
188
|
+
props[:table_header] = table_header if table_header
|
189
|
+
props[:table_body] = table_body if table_body
|
190
|
+
props[:description] = description if description
|
191
|
+
Cucumber::Messages::GherkinDocument::Feature::Scenario::Examples.new(props)
|
192
|
+
when :ExamplesTable
|
193
|
+
get_table_rows(node)
|
194
|
+
when :Description
|
195
|
+
line_tokens = node.get_tokens(:Other)
|
196
|
+
# Trim trailing empty lines
|
197
|
+
last_non_empty = line_tokens.rindex { |token| !token.line.trimmed_line_text.empty? }
|
198
|
+
description = line_tokens[0..last_non_empty].map { |token| token.matched_text }.join("\n")
|
199
|
+
return description
|
200
|
+
when :Feature
|
201
|
+
header = node.get_single(:FeatureHeader)
|
202
|
+
return unless header
|
203
|
+
tags = get_tags(header)
|
204
|
+
feature_line = header.get_token(:FeatureLine)
|
205
|
+
return unless feature_line
|
206
|
+
children = []
|
207
|
+
background = node.get_single(:Background)
|
208
|
+
children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild.new(background: background)) if background
|
209
|
+
node.get_items(:ScenarioDefinition).each do |scenario|
|
210
|
+
children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild.new(scenario: scenario))
|
211
|
+
end
|
212
|
+
node.get_items(:Rule).each do |rule|
|
213
|
+
children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild.new(rule: rule))
|
214
|
+
end
|
215
|
+
description = get_description(header)
|
216
|
+
language = feature_line.matched_gherkin_dialect
|
217
|
+
|
218
|
+
props = {
|
219
|
+
tags: tags,
|
220
|
+
location: get_location(feature_line, 0),
|
221
|
+
language: language,
|
222
|
+
keyword: feature_line.matched_keyword,
|
223
|
+
name: feature_line.matched_text,
|
224
|
+
children: children,
|
225
|
+
}
|
226
|
+
props[:description] = description if description
|
227
|
+
|
228
|
+
Cucumber::Messages::GherkinDocument::Feature.new(props)
|
229
|
+
when :Rule
|
230
|
+
header = node.get_single(:RuleHeader)
|
231
|
+
return unless header
|
232
|
+
rule_line = header.get_token(:RuleLine)
|
233
|
+
return unless rule_line
|
234
|
+
children = []
|
235
|
+
background = node.get_single(:Background)
|
236
|
+
children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild::RuleChild.new(background: background)) if background
|
237
|
+
node.get_items(:ScenarioDefinition).each do |scenario|
|
238
|
+
children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild::RuleChild.new(scenario: scenario))
|
239
|
+
end
|
240
|
+
description = get_description(header)
|
241
|
+
|
242
|
+
Cucumber::Messages::GherkinDocument::Feature::FeatureChild::Rule.new(
|
243
|
+
location: get_location(rule_line, 0),
|
244
|
+
keyword: rule_line.matched_keyword,
|
245
|
+
name: rule_line.matched_text,
|
246
|
+
description: description,
|
247
|
+
children: children,
|
248
|
+
)
|
249
|
+
when :GherkinDocument
|
250
|
+
feature = node.get_single(:Feature)
|
251
|
+
{
|
252
|
+
feature: feature,
|
253
|
+
comments: @comments
|
254
|
+
}
|
255
|
+
else
|
256
|
+
return node
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Gherkin
|
2
|
+
class AstNode
|
3
|
+
attr_reader :rule_type
|
4
|
+
|
5
|
+
def initialize(rule_type)
|
6
|
+
@rule_type = rule_type
|
7
|
+
@_sub_items = Hash.new { |hash, key| hash[key] = [] } # returns [] for unknown key
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(rule_type, obj)
|
11
|
+
@_sub_items[rule_type].push(obj)
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_single(rule_type)
|
15
|
+
@_sub_items[rule_type].first
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_items(rule_type)
|
19
|
+
@_sub_items[rule_type]
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_token(token_type)
|
23
|
+
get_single(token_type)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_tokens(token_type)
|
27
|
+
@_sub_items[token_type]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/gherkin/dialect.rb
CHANGED
@@ -1,18 +1,8 @@
|
|
1
1
|
require 'json'
|
2
|
-
require 'open3'
|
3
|
-
require 'c21e/exe_file'
|
4
|
-
require 'gherkin/exe_file_path'
|
5
2
|
|
6
3
|
module Gherkin
|
7
|
-
|
8
|
-
|
9
|
-
data, = Open3.capture2e(gherkin_executable, '--dialects')
|
10
|
-
data
|
11
|
-
end
|
12
|
-
|
13
|
-
private_class_method :dialects_json
|
14
|
-
|
15
|
-
DIALECTS = JSON.parse(dialects_json)
|
4
|
+
DIALECT_FILE_PATH = File.expand_path("gherkin-languages.json", File.dirname(__FILE__))
|
5
|
+
DIALECTS = JSON.parse File.open(DIALECT_FILE_PATH, 'r:UTF-8').read
|
16
6
|
|
17
7
|
class Dialect
|
18
8
|
def self.for(name)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Gherkin
|
2
|
+
class ParserError < StandardError; end
|
3
|
+
|
4
|
+
class ParserException < ParserError
|
5
|
+
attr_reader :location
|
6
|
+
|
7
|
+
def initialize(message, location)
|
8
|
+
@location = location
|
9
|
+
super("(#{location[:line]}:#{location[:column] || 0}): #{message}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class NoSuchLanguageException < ParserException
|
14
|
+
def initialize(language, location)
|
15
|
+
super "Language not supported: #{language}", location
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class AstBuilderException < ParserException; end
|
20
|
+
|
21
|
+
class CompositeParserException < ParserError
|
22
|
+
attr_reader :errors
|
23
|
+
|
24
|
+
def initialize(errors)
|
25
|
+
@errors = errors
|
26
|
+
super "Parser errors:\n" + errors.map(&:message).join("\n")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class UnexpectedTokenException < ParserException
|
31
|
+
def initialize(received_token, expected_token_types, state_comment)
|
32
|
+
message = "expected: #{expected_token_types.join(", ")}, got '#{received_token.token_value.strip}'"
|
33
|
+
column = received_token.location[:column]
|
34
|
+
location = (column.nil? || column.zero?) ? {line: received_token.location[:line], column: received_token.line.indent + 1} : received_token.location
|
35
|
+
super(message, location)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class UnexpectedEOFException < ParserException
|
40
|
+
def initialize(received_token, expected_token_types, state_comment)
|
41
|
+
message = "unexpected end of file, expected: #{expected_token_types.join(", ")}"
|
42
|
+
super(message, received_token.location)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|