asciidoctor 0.0.7 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of asciidoctor might be problematic. Click here for more details.
- data/Gemfile +2 -0
- data/README.asciidoc +35 -26
- data/Rakefile +9 -6
- data/asciidoctor.gemspec +27 -8
- data/bin/asciidoctor +1 -1
- data/lib/asciidoctor.rb +351 -63
- data/lib/asciidoctor/abstract_block.rb +218 -0
- data/lib/asciidoctor/abstract_node.rb +249 -0
- data/lib/asciidoctor/attribute_list.rb +211 -0
- data/lib/asciidoctor/backends/base_template.rb +99 -0
- data/lib/asciidoctor/backends/docbook45.rb +510 -0
- data/lib/asciidoctor/backends/html5.rb +585 -0
- data/lib/asciidoctor/block.rb +27 -254
- data/lib/asciidoctor/callouts.rb +117 -0
- data/lib/asciidoctor/debug.rb +7 -4
- data/lib/asciidoctor/document.rb +229 -77
- data/lib/asciidoctor/inline.rb +29 -0
- data/lib/asciidoctor/lexer.rb +1330 -502
- data/lib/asciidoctor/list_item.rb +33 -34
- data/lib/asciidoctor/reader.rb +305 -142
- data/lib/asciidoctor/renderer.rb +115 -19
- data/lib/asciidoctor/section.rb +100 -189
- data/lib/asciidoctor/substituters.rb +468 -0
- data/lib/asciidoctor/table.rb +499 -0
- data/lib/asciidoctor/version.rb +1 -1
- data/test/attributes_test.rb +301 -87
- data/test/blocks_test.rb +568 -0
- data/test/document_test.rb +221 -24
- data/test/fixtures/dot.gif +0 -0
- data/test/fixtures/encoding.asciidoc +1 -0
- data/test/fixtures/include-file.asciidoc +1 -0
- data/test/fixtures/tip.gif +0 -0
- data/test/headers_test.rb +411 -43
- data/test/lexer_test.rb +265 -45
- data/test/links_test.rb +144 -3
- data/test/lists_test.rb +2252 -74
- data/test/paragraphs_test.rb +21 -30
- data/test/preamble_test.rb +24 -0
- data/test/reader_test.rb +248 -12
- data/test/renderer_test.rb +22 -0
- data/test/substitutions_test.rb +414 -0
- data/test/tables_test.rb +484 -0
- data/test/test_helper.rb +70 -6
- data/test/text_test.rb +30 -6
- metadata +64 -10
- data/lib/asciidoctor/render_templates.rb +0 -317
- data/lib/asciidoctor/string.rb +0 -12
data/lib/asciidoctor/version.rb
CHANGED
data/test/attributes_test.rb
CHANGED
@@ -1,91 +1,247 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
context
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
context 'Attributes' do
|
4
|
+
context 'Assignment' do
|
5
|
+
test 'creates an attribute' do
|
6
|
+
doc = document_from_string(':frog: Tanglefoot')
|
7
|
+
assert_equal 'Tanglefoot', doc.attributes['frog']
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
10
|
+
test 'creates an attribute by fusing a multi-line value' do
|
11
|
+
str = <<-EOS
|
11
12
|
:description: This is the first +
|
12
13
|
Ruby implementation of +
|
13
14
|
AsciiDoc.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
EOS
|
16
|
+
doc = document_from_string(str)
|
17
|
+
assert_equal 'This is the first Ruby implementation of AsciiDoc.', doc.attributes['description']
|
18
|
+
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
test 'deletes an attribute' do
|
21
|
+
doc = document_from_string(":frog: Tanglefoot\n:frog!:")
|
22
|
+
assert_equal nil, doc.attributes['frog']
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
test "doesn't choke when deleting a non-existing attribute" do
|
26
|
+
doc = document_from_string(':frog!:')
|
27
|
+
assert_equal nil, doc.attributes['frog']
|
28
|
+
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
30
|
+
test "replaces special characters in attribute value" do
|
31
|
+
doc = document_from_string(":xml-busters: <>&")
|
32
|
+
assert_equal '<>&', doc.attributes['xml-busters']
|
33
|
+
end
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
35
|
+
test "performs attribute substitution on attribute value" do
|
36
|
+
doc = document_from_string(":version: 1.0\n:release: Asciidoctor {version}")
|
37
|
+
assert_equal 'Asciidoctor 1.0', doc.attributes['release']
|
38
|
+
end
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
40
|
+
test "assigns attribute to empty string if substitution fails to resolve attribute" do
|
41
|
+
doc = document_from_string(":release: Asciidoctor {version}")
|
42
|
+
assert_equal '', doc.attributes['release']
|
43
|
+
end
|
46
44
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
# html = render_string("Look, a {gobbledygook}")
|
52
|
-
# result = Nokogiri::HTML(html)
|
53
|
-
# assert_equal("Look, a {gobbledygook}", result.css("p").first.content.strip)
|
54
|
-
# end
|
55
|
-
|
56
|
-
test "substitutes inside unordered list items" do
|
57
|
-
html = render_string(":foo: bar\n* snort at the {foo}\n* yawn")
|
58
|
-
result = Nokogiri::HTML(html)
|
59
|
-
assert_match /snort at the bar/, result.css("li").first.content.strip
|
60
|
-
end
|
45
|
+
test "assigns multi-line attribute to empty string if substitution fails to resolve attribute" do
|
46
|
+
doc = document_from_string(":release: Asciidoctor +\n {version}")
|
47
|
+
assert_equal '', doc.attributes['release']
|
48
|
+
end
|
61
49
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
50
|
+
test "apply custom substitutions to text in passthrough macro and assign to attribute" do
|
51
|
+
doc = document_from_string(":xml-busters: pass:[<>&]")
|
52
|
+
assert_equal '<>&', doc.attributes['xml-busters']
|
53
|
+
doc = document_from_string(":xml-busters: pass:none[<>&]")
|
54
|
+
assert_equal '<>&', doc.attributes['xml-busters']
|
55
|
+
doc = document_from_string(":xml-busters: pass:specialcharacters[<>&]")
|
56
|
+
assert_equal '<>&', doc.attributes['xml-busters']
|
57
|
+
end
|
69
58
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
59
|
+
test "attribute is treated as defined until it's not" do
|
60
|
+
input = <<-EOS
|
61
|
+
:holygrail:
|
62
|
+
ifdef::holygrail[]
|
63
|
+
The holy grail has been found!
|
64
|
+
endif::holygrail[]
|
65
|
+
|
66
|
+
:holygrail!:
|
67
|
+
ifndef::holygrail[]
|
68
|
+
Buggers! What happened to the grail?
|
69
|
+
endif::holygrail[]
|
70
|
+
EOS
|
71
|
+
output = render_string input
|
72
|
+
assert_xpath '//p', output, 2
|
73
|
+
assert_xpath '(//p)[1][text() = "The holy grail has been found!"]', output, 1
|
74
|
+
assert_xpath '(//p)[2][text() = "Buggers! What happened to the grail?"]', output, 1
|
75
|
+
end
|
76
|
+
|
77
|
+
# Validates requirement: "Header attributes are overridden by command-line attributes."
|
78
|
+
test 'attribute defined in document options overrides attribute in document' do
|
79
|
+
doc = document_from_string(':cash: money', :attributes => {'cash' => 'heroes'})
|
80
|
+
assert_equal 'heroes', doc.attributes['cash']
|
81
|
+
end
|
82
|
+
|
83
|
+
test 'attribute defined in document options cannot be unassigned in document' do
|
84
|
+
doc = document_from_string(':cash!:', :attributes => {'cash' => 'heroes'})
|
85
|
+
assert_equal 'heroes', doc.attributes['cash']
|
86
|
+
end
|
87
|
+
|
88
|
+
test 'attribute undefined in document options cannot be assigned in document' do
|
89
|
+
doc = document_from_string(':cash: money', :attributes => {'cash!' => 1 })
|
90
|
+
assert_equal nil, doc.attributes['cash']
|
91
|
+
doc = document_from_string(':cash: money', :attributes => {'cash' => nil })
|
92
|
+
assert_equal nil, doc.attributes['cash']
|
93
|
+
end
|
94
|
+
|
95
|
+
test 'backend attributes are updated if backend attribute is defined in document' do
|
96
|
+
doc = document_from_string(':backend: docbook45')
|
97
|
+
assert_equal 'docbook45', doc.attributes['backend']
|
98
|
+
assert doc.attributes.has_key? 'backend-docbook45'
|
99
|
+
assert_equal 'docbook', doc.attributes['basebackend']
|
100
|
+
assert doc.attributes.has_key? 'basebackend-docbook'
|
101
|
+
end
|
102
|
+
|
103
|
+
test 'backend attributes defined in document options overrides backend attribute in document' do
|
104
|
+
doc = document_from_string(':backend: docbook45', :attributes => {'backend' => 'html5'})
|
105
|
+
assert_equal 'html5', doc.attributes['backend']
|
106
|
+
assert doc.attributes.has_key? 'backend-html5'
|
107
|
+
assert_equal 'html', doc.attributes['basebackend']
|
108
|
+
assert doc.attributes.has_key? 'basebackend-html'
|
109
|
+
end
|
75
110
|
|
76
|
-
test "doesn't disturb attribute-looking things escaped with literals" do
|
77
|
-
html = render_string(":foo: bar\nThis is a +++{foo}+++ day.")
|
78
|
-
result = Nokogiri::HTML(html)
|
79
|
-
#assert_equal 'This is a {foo} day.', result.css('p').first.content.strip
|
80
|
-
pending "Don't yet have inline passthrough working"
|
81
111
|
end
|
82
112
|
|
83
|
-
|
84
|
-
|
113
|
+
context 'Interpolation' do
|
114
|
+
|
115
|
+
test "render properly with simple names" do
|
116
|
+
html = render_string(":frog: Tanglefoot\n:my_super-hero: Spiderman\n\nYo, {frog}!\nBeat {my_super-hero}!")
|
117
|
+
result = Nokogiri::HTML(html)
|
118
|
+
assert_equal "Yo, Tanglefoot!\nBeat Spiderman!", result.css("p").first.content.strip
|
119
|
+
end
|
120
|
+
|
121
|
+
test "render properly with single character name" do
|
122
|
+
html = render_string(":r: Ruby\n\nR is for {r}!")
|
123
|
+
result = Nokogiri::HTML(html)
|
124
|
+
assert_equal 'R is for Ruby!', result.css("p").first.content.strip
|
125
|
+
end
|
126
|
+
|
127
|
+
test "convert multi-word names and render" do
|
128
|
+
html = render_string("Main Header\n===========\n:My frog: Tanglefoot\n\nYo, {myfrog}!")
|
129
|
+
result = Nokogiri::HTML(html)
|
130
|
+
assert_equal 'Yo, Tanglefoot!', result.css("p").first.content.strip
|
131
|
+
end
|
132
|
+
|
133
|
+
test "ignores lines with bad attributes" do
|
134
|
+
html = render_string("This is\nblah blah {foobarbaz}\nall there is.")
|
135
|
+
result = Nokogiri::HTML(html)
|
136
|
+
assert_no_match(/blah blah/m, result.css("p").first.content.strip)
|
137
|
+
end
|
138
|
+
|
139
|
+
test "attribute value gets interpretted when rendering" do
|
140
|
+
doc = document_from_string(":google: http://google.com[Google]\n\n{google}")
|
141
|
+
assert_equal 'http://google.com[Google]', doc.attributes['google']
|
142
|
+
output = doc.render
|
143
|
+
assert_xpath '//a[@href="http://google.com"][text() = "Google"]', output, 1
|
144
|
+
end
|
145
|
+
|
146
|
+
# See above - AsciiDoc says we're supposed to delete lines with bad
|
147
|
+
# attribute refs in them. AsciiDoc is strange.
|
148
|
+
#
|
149
|
+
# test "Unknowns" do
|
150
|
+
# html = render_string("Look, a {gobbledygook}")
|
151
|
+
# result = Nokogiri::HTML(html)
|
152
|
+
# assert_equal("Look, a {gobbledygook}", result.css("p").first.content.strip)
|
153
|
+
# end
|
154
|
+
|
155
|
+
test "substitutes inside unordered list items" do
|
156
|
+
html = render_string(":foo: bar\n* snort at the {foo}\n* yawn")
|
157
|
+
result = Nokogiri::HTML(html)
|
158
|
+
assert_match(/snort at the bar/, result.css("li").first.content.strip)
|
159
|
+
end
|
160
|
+
|
161
|
+
test 'substitutes inside section title' do
|
162
|
+
output = render_string(":prefix: Cool\n\n== {prefix} Title\n\ncontent")
|
163
|
+
result = Nokogiri::HTML(output)
|
164
|
+
assert_match(/Cool Title/, result.css('h2').first.content)
|
165
|
+
assert_match(/_cool_title/, result.css('h2').first.attr('id'))
|
166
|
+
end
|
167
|
+
|
168
|
+
test 'substitutes inside block title' do
|
169
|
+
input = <<-EOS
|
170
|
+
:gem_name: asciidoctor
|
171
|
+
|
172
|
+
.Require the +{gem_name}+ gem
|
173
|
+
To use {gem_name}, the first thing to do is to import it in your Ruby source file.
|
174
|
+
EOS
|
175
|
+
output = render_embedded_string input
|
176
|
+
assert_xpath '//*[@class="title"]/tt[text()="asciidoctor"]', output, 1
|
177
|
+
end
|
178
|
+
|
179
|
+
test 'renders attribute until it is deleted' do
|
180
|
+
pending 'This requires that we consume attributes as the document is being lexed, not up front'
|
181
|
+
#output = render_string(":foo: bar\n\nCrossing the {foo}\n\n:foo!:\nBelly up to the {foo}")
|
182
|
+
# result = Nokogiri::HTML(html)
|
183
|
+
# assert_match /Crossing the bar/, result.css("p").first.content.strip
|
184
|
+
# assert_no_match /Belly up to the bar/, result.css("p").last.content.strip
|
185
|
+
end
|
186
|
+
|
187
|
+
test 'does not disturb attribute-looking things escaped with backslash' do
|
188
|
+
html = render_string(":foo: bar\nThis is a \\{foo} day.")
|
189
|
+
result = Nokogiri::HTML(html)
|
190
|
+
assert_equal 'This is a {foo} day.', result.css('p').first.content.strip
|
191
|
+
end
|
192
|
+
|
193
|
+
test 'does not disturb attribute-looking things escaped with literals' do
|
194
|
+
html = render_string(":foo: bar\nThis is a +++{foo}+++ day.")
|
195
|
+
result = Nokogiri::HTML(html)
|
196
|
+
assert_equal 'This is a {foo} day.', result.css('p').first.content.strip
|
197
|
+
end
|
198
|
+
|
199
|
+
test 'does not substitute attributes inside listing blocks' do
|
200
|
+
input = <<-EOS
|
201
|
+
:forecast: snow
|
202
|
+
|
203
|
+
----
|
204
|
+
puts 'The forecast for today is {forecast}'
|
205
|
+
----
|
206
|
+
EOS
|
207
|
+
output = render_string(input)
|
208
|
+
assert_match(/\{forecast\}/, output)
|
209
|
+
end
|
210
|
+
|
211
|
+
test 'does not substitute attributes inside literal blocks' do
|
212
|
+
input = <<-EOS
|
213
|
+
:foo: bar
|
214
|
+
|
215
|
+
....
|
216
|
+
You insert the text {foo} to expand the value
|
217
|
+
of the attribute named foo in your document.
|
218
|
+
....
|
219
|
+
EOS
|
220
|
+
output = render_string(input)
|
221
|
+
assert_match(/\{foo\}/, output)
|
222
|
+
end
|
85
223
|
end
|
86
224
|
|
87
|
-
|
88
|
-
|
225
|
+
context "Intrinsic attributes" do
|
226
|
+
|
227
|
+
test "substitute intrinsics" do
|
228
|
+
Asciidoctor::INTRINSICS.each_pair do |key, value|
|
229
|
+
html = render_string("Look, a {#{key}} is here")
|
230
|
+
# can't use Nokogiri because it interprets the HTML entities and we can't match them
|
231
|
+
assert_match(/Look, a #{Regexp.escape(value)} is here/, html)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
test "don't escape intrinsic substitutions" do
|
236
|
+
html = render_string('happy{nbsp}together')
|
237
|
+
assert_match(/happy together/, html)
|
238
|
+
end
|
239
|
+
|
240
|
+
test "escape special characters" do
|
241
|
+
html = render_string('<node>&</node>')
|
242
|
+
assert_match(/<node>&<\/node>/, html)
|
243
|
+
end
|
244
|
+
|
89
245
|
end
|
90
246
|
|
91
247
|
context "Block attributes" do
|
@@ -97,13 +253,40 @@ A famous quote.
|
|
97
253
|
____
|
98
254
|
EOS
|
99
255
|
doc = document_from_string(input)
|
100
|
-
qb = doc.
|
256
|
+
qb = doc.blocks.first
|
101
257
|
assert_equal 'quote', qb.attributes['style']
|
102
258
|
assert_equal 'quote', qb.attr(:style)
|
103
259
|
assert_equal 'Name', qb.attributes['attribution']
|
104
260
|
assert_equal 'Source', qb.attributes['citetitle']
|
105
261
|
end
|
106
262
|
|
263
|
+
test "Normal substitutions are performed on single-quoted attributes" do
|
264
|
+
input = <<-EOS
|
265
|
+
[quote, Name, 'http://wikipedia.org[Source]']
|
266
|
+
____
|
267
|
+
A famous quote.
|
268
|
+
____
|
269
|
+
EOS
|
270
|
+
doc = document_from_string(input)
|
271
|
+
qb = doc.blocks.first
|
272
|
+
assert_equal 'quote', qb.attributes['style']
|
273
|
+
assert_equal 'quote', qb.attr(:style)
|
274
|
+
assert_equal 'Name', qb.attributes['attribution']
|
275
|
+
assert_equal '<a href="http://wikipedia.org">Source</a>', qb.attributes['citetitle']
|
276
|
+
end
|
277
|
+
|
278
|
+
test "Attribute substitutions are performed on attribute list before parsing attributes" do
|
279
|
+
input = <<-EOS
|
280
|
+
:lead: role="lead"
|
281
|
+
|
282
|
+
[{lead}]
|
283
|
+
A paragraph
|
284
|
+
EOS
|
285
|
+
doc = document_from_string(input)
|
286
|
+
para = doc.blocks.first
|
287
|
+
assert_equal 'lead', para.attributes['role']
|
288
|
+
end
|
289
|
+
|
107
290
|
test "Block attributes are additive" do
|
108
291
|
input = <<-EOS
|
109
292
|
[id='foo']
|
@@ -111,32 +294,63 @@ ____
|
|
111
294
|
A paragraph.
|
112
295
|
EOS
|
113
296
|
doc = document_from_string(input)
|
114
|
-
para = doc.
|
297
|
+
para = doc.blocks.first
|
115
298
|
assert_equal 'foo', para.id
|
116
299
|
assert_equal 'lead', para.attributes['role']
|
117
300
|
end
|
118
|
-
end
|
119
301
|
|
120
|
-
|
302
|
+
test "Last wins for id attribute" do
|
303
|
+
input = <<-EOS
|
304
|
+
[[bar]]
|
305
|
+
[[foo]]
|
306
|
+
== Section
|
121
307
|
|
122
|
-
|
123
|
-
Asciidoctor::INTRINSICS.each_pair do |key, value|
|
124
|
-
html = render_string("Look, a {#{key}} is here")
|
125
|
-
# can't use Nokogiri because it interprets the HTML entities and we can't match them
|
126
|
-
assert_match /Look, a #{Regexp.escape(value)} is here/, html
|
127
|
-
end
|
128
|
-
end
|
308
|
+
paragraph
|
129
309
|
|
130
|
-
|
131
|
-
|
132
|
-
|
310
|
+
[[baz]]
|
311
|
+
[id='coolio']
|
312
|
+
=== Section
|
313
|
+
EOS
|
314
|
+
doc = document_from_string(input)
|
315
|
+
sec = doc.first_section
|
316
|
+
assert_equal 'foo', sec.id
|
317
|
+
subsec = sec.blocks.last
|
318
|
+
assert_equal 'coolio', subsec.id
|
133
319
|
end
|
134
320
|
|
135
|
-
test "
|
136
|
-
|
137
|
-
|
321
|
+
test "trailing block attributes tranfer to the following section" do
|
322
|
+
input = <<-EOS
|
323
|
+
[[one]]
|
324
|
+
|
325
|
+
== Section One
|
326
|
+
|
327
|
+
paragraph
|
328
|
+
|
329
|
+
[[sub]]
|
330
|
+
// try to mess this up!
|
331
|
+
|
332
|
+
=== Sub-section
|
333
|
+
|
334
|
+
paragraph
|
335
|
+
|
336
|
+
[role='classy']
|
337
|
+
|
338
|
+
////
|
339
|
+
block comment
|
340
|
+
////
|
341
|
+
|
342
|
+
== Section Two
|
343
|
+
|
344
|
+
content
|
345
|
+
EOS
|
346
|
+
doc = document_from_string(input)
|
347
|
+
section_one = doc.blocks.first
|
348
|
+
assert_equal 'one', section_one.id
|
349
|
+
subsection = section_one.blocks.last
|
350
|
+
assert_equal 'sub', subsection.id
|
351
|
+
section_two = doc.blocks.last
|
352
|
+
assert_equal 'classy', section_two.attr(:role)
|
138
353
|
end
|
139
|
-
|
140
354
|
end
|
141
355
|
|
142
356
|
end
|
data/test/blocks_test.rb
ADDED
@@ -0,0 +1,568 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
context "Blocks" do
|
4
|
+
context "Rulers" do
|
5
|
+
test "ruler" do
|
6
|
+
output = render_string("'''")
|
7
|
+
assert_xpath '//*[@id="content"]/hr', output, 1
|
8
|
+
assert_xpath '//*[@id="content"]/*', output, 1
|
9
|
+
end
|
10
|
+
|
11
|
+
test "ruler between blocks" do
|
12
|
+
output = render_string("Block above\n\n'''\n\nBlock below")
|
13
|
+
assert_xpath '//*[@id="content"]/hr', output, 1
|
14
|
+
assert_xpath '//*[@id="content"]/hr/preceding-sibling::*', output, 1
|
15
|
+
assert_xpath '//*[@id="content"]/hr/following-sibling::*', output, 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'Comments' do
|
20
|
+
test 'line comment between paragraphs offset by blank lines' do
|
21
|
+
input = <<-EOS
|
22
|
+
first paragraph
|
23
|
+
|
24
|
+
// line comment
|
25
|
+
|
26
|
+
second paragraph
|
27
|
+
EOS
|
28
|
+
output = render_embedded_string input
|
29
|
+
assert_no_match(/line comment/, output)
|
30
|
+
assert_xpath '//p', output, 2
|
31
|
+
end
|
32
|
+
|
33
|
+
test 'adjacent line comment between paragraphs' do
|
34
|
+
input = <<-EOS
|
35
|
+
first line
|
36
|
+
// line comment
|
37
|
+
second line
|
38
|
+
EOS
|
39
|
+
output = render_embedded_string input
|
40
|
+
assert_no_match(/line comment/, output)
|
41
|
+
assert_xpath '//p', output, 1
|
42
|
+
assert_xpath "//p[1][text()='first line\nsecond line']", output, 1
|
43
|
+
end
|
44
|
+
|
45
|
+
test 'comment block between paragraphs offset by blank lines' do
|
46
|
+
input = <<-EOS
|
47
|
+
first paragraph
|
48
|
+
|
49
|
+
////
|
50
|
+
block comment
|
51
|
+
////
|
52
|
+
|
53
|
+
second paragraph
|
54
|
+
EOS
|
55
|
+
output = render_embedded_string input
|
56
|
+
assert_no_match(/block comment/, output)
|
57
|
+
assert_xpath '//p', output, 2
|
58
|
+
end
|
59
|
+
|
60
|
+
test 'adjacent comment block between paragraphs' do
|
61
|
+
input = <<-EOS
|
62
|
+
first paragraph
|
63
|
+
////
|
64
|
+
block comment
|
65
|
+
////
|
66
|
+
second paragraph
|
67
|
+
EOS
|
68
|
+
output = render_embedded_string input
|
69
|
+
assert_no_match(/block comment/, output)
|
70
|
+
assert_xpath '//p', output, 2
|
71
|
+
end
|
72
|
+
|
73
|
+
test "can render with block comment at end of document with trailing endlines" do
|
74
|
+
input = <<-EOS
|
75
|
+
paragraph
|
76
|
+
|
77
|
+
////
|
78
|
+
block comment
|
79
|
+
////
|
80
|
+
|
81
|
+
|
82
|
+
EOS
|
83
|
+
output = render_embedded_string input
|
84
|
+
assert_no_match(/block comment/, output)
|
85
|
+
end
|
86
|
+
|
87
|
+
test "trailing endlines after block comment at end of document does not create paragraph" do
|
88
|
+
input = <<-EOS
|
89
|
+
paragraph
|
90
|
+
|
91
|
+
////
|
92
|
+
block comment
|
93
|
+
////
|
94
|
+
|
95
|
+
|
96
|
+
EOS
|
97
|
+
d = document_from_string input
|
98
|
+
assert_equal 1, d.blocks.size
|
99
|
+
assert_xpath '//p', d.render, 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context "Example Blocks" do
|
104
|
+
test "can render example block" do
|
105
|
+
input = <<-EOS
|
106
|
+
====
|
107
|
+
This is an example of an example block.
|
108
|
+
|
109
|
+
How crazy is that?
|
110
|
+
====
|
111
|
+
EOS
|
112
|
+
|
113
|
+
output = render_string input
|
114
|
+
assert_xpath '//*[@class="exampleblock"]//p', output, 2
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context "Preformatted Blocks" do
|
119
|
+
test 'should separate adjacent paragraphs and listing into blocks' do
|
120
|
+
input = <<-EOS
|
121
|
+
paragraph 1
|
122
|
+
----
|
123
|
+
listing content
|
124
|
+
----
|
125
|
+
paragraph 2
|
126
|
+
EOS
|
127
|
+
|
128
|
+
output = render_embedded_string input
|
129
|
+
assert_xpath '/*[@class="paragraph"]/p', output, 2
|
130
|
+
assert_xpath '/*[@class="listingblock"]', output, 1
|
131
|
+
assert_xpath '(/*[@class="paragraph"]/following-sibling::*)[1][@class="listingblock"]', output, 1
|
132
|
+
end
|
133
|
+
|
134
|
+
test "should preserve endlines in literal block" do
|
135
|
+
input = <<-EOS
|
136
|
+
....
|
137
|
+
line one
|
138
|
+
|
139
|
+
line two
|
140
|
+
|
141
|
+
line three
|
142
|
+
....
|
143
|
+
EOS
|
144
|
+
[true, false].each {|compact|
|
145
|
+
output = render_string input, :compact => compact
|
146
|
+
assert_xpath '//pre', output, 1
|
147
|
+
assert_xpath '//pre/text()', output, 1
|
148
|
+
text = xmlnodes_at_xpath('//pre/text()', output, 1).text
|
149
|
+
lines = text.lines.entries
|
150
|
+
assert_equal 5, lines.size
|
151
|
+
expected = "line one\n\nline two\n\nline three".lines.entries
|
152
|
+
assert_equal expected, lines
|
153
|
+
blank_lines = output.scan(/\n[[:blank:]]*\n/).size
|
154
|
+
if compact
|
155
|
+
assert_equal 2, blank_lines
|
156
|
+
else
|
157
|
+
assert blank_lines > 2
|
158
|
+
end
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
test "should preserve endlines in listing block" do
|
163
|
+
input = <<-EOS
|
164
|
+
[source]
|
165
|
+
----
|
166
|
+
line one
|
167
|
+
|
168
|
+
line two
|
169
|
+
|
170
|
+
line three
|
171
|
+
----
|
172
|
+
EOS
|
173
|
+
[true, false].each {|compact|
|
174
|
+
output = render_string input, :compact => compact
|
175
|
+
assert_xpath '//pre/code', output, 1
|
176
|
+
assert_xpath '//pre/code/text()', output, 1
|
177
|
+
text = xmlnodes_at_xpath('//pre/code/text()', output, 1).text
|
178
|
+
lines = text.lines.entries
|
179
|
+
assert_equal 5, lines.size
|
180
|
+
expected = "line one\n\nline two\n\nline three".lines.entries
|
181
|
+
assert_equal expected, lines
|
182
|
+
blank_lines = output.scan(/\n[[:blank:]]*\n/).size
|
183
|
+
if compact
|
184
|
+
assert_equal 2, blank_lines
|
185
|
+
else
|
186
|
+
assert blank_lines > 2
|
187
|
+
end
|
188
|
+
}
|
189
|
+
end
|
190
|
+
|
191
|
+
test "should preserve endlines in verse block" do
|
192
|
+
input = <<-EOS
|
193
|
+
[verse]
|
194
|
+
____
|
195
|
+
line one
|
196
|
+
|
197
|
+
line two
|
198
|
+
|
199
|
+
line three
|
200
|
+
____
|
201
|
+
EOS
|
202
|
+
[true, false].each {|compact|
|
203
|
+
output = render_string input, :compact => compact
|
204
|
+
assert_xpath '//*[@class="verseblock"]/pre', output, 1
|
205
|
+
assert_xpath '//*[@class="verseblock"]/pre/text()', output, 1
|
206
|
+
text = xmlnodes_at_xpath('//*[@class="verseblock"]/pre/text()', output, 1).text
|
207
|
+
lines = text.lines.entries
|
208
|
+
assert_equal 5, lines.size
|
209
|
+
expected = "line one\n\nline two\n\nline three".lines.entries
|
210
|
+
assert_equal expected, lines
|
211
|
+
blank_lines = output.scan(/\n[[:blank:]]*\n/).size
|
212
|
+
if compact
|
213
|
+
assert_equal 2, blank_lines
|
214
|
+
else
|
215
|
+
assert blank_lines > 2
|
216
|
+
end
|
217
|
+
}
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
context "Open Blocks" do
|
222
|
+
test "can render open block" do
|
223
|
+
input = <<-EOS
|
224
|
+
--
|
225
|
+
This is an open block.
|
226
|
+
|
227
|
+
It can span multiple lines.
|
228
|
+
--
|
229
|
+
EOS
|
230
|
+
|
231
|
+
output = render_string input
|
232
|
+
assert_xpath '//*[@class="openblock"]//p', output, 2
|
233
|
+
end
|
234
|
+
|
235
|
+
test "open block can contain another block" do
|
236
|
+
input = <<-EOS
|
237
|
+
--
|
238
|
+
This is an open block.
|
239
|
+
|
240
|
+
It can span multiple lines.
|
241
|
+
|
242
|
+
____
|
243
|
+
It can hold great quotes like this one.
|
244
|
+
____
|
245
|
+
--
|
246
|
+
EOS
|
247
|
+
|
248
|
+
output = render_string input
|
249
|
+
assert_xpath '//*[@class="openblock"]//p', output, 3
|
250
|
+
assert_xpath '//*[@class="openblock"]//*[@class="quoteblock"]', output, 1
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
context 'Passthrough Blocks' do
|
255
|
+
test 'can parse a passthrough block' do
|
256
|
+
input = <<-EOS
|
257
|
+
++++
|
258
|
+
This is a passthrough block.
|
259
|
+
++++
|
260
|
+
EOS
|
261
|
+
|
262
|
+
block = block_from_string input
|
263
|
+
assert !block.nil?
|
264
|
+
assert_equal 1, block.buffer.size
|
265
|
+
assert_equal 'This is a passthrough block.', block.buffer.first
|
266
|
+
end
|
267
|
+
|
268
|
+
test 'performs passthrough subs on a passthrough block' do
|
269
|
+
input = <<-EOS
|
270
|
+
:type: passthrough
|
271
|
+
|
272
|
+
++++
|
273
|
+
This is a '{type}' block.
|
274
|
+
http://asciidoc.org
|
275
|
+
++++
|
276
|
+
EOS
|
277
|
+
|
278
|
+
expected = %(This is a 'passthrough' block.\n<a href="http://asciidoc.org">http://asciidoc.org</a>)
|
279
|
+
output = render_embedded_string input
|
280
|
+
assert_equal expected, output.strip
|
281
|
+
end
|
282
|
+
|
283
|
+
test 'passthrough block honors explicit subs list' do
|
284
|
+
input = <<-EOS
|
285
|
+
:type: passthrough
|
286
|
+
|
287
|
+
[subs="attributes, quotes"]
|
288
|
+
++++
|
289
|
+
This is a '{type}' block.
|
290
|
+
http://asciidoc.org
|
291
|
+
++++
|
292
|
+
EOS
|
293
|
+
|
294
|
+
expected = %(This is a <em>passthrough</em> block.\nhttp://asciidoc.org)
|
295
|
+
output = render_embedded_string input
|
296
|
+
assert_equal expected, output.strip
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
context 'Metadata' do
|
301
|
+
test 'block title above section gets carried over to first block in section' do
|
302
|
+
input = <<-EOS
|
303
|
+
.Title
|
304
|
+
== Section
|
305
|
+
|
306
|
+
paragraph
|
307
|
+
EOS
|
308
|
+
output = render_string input
|
309
|
+
assert_xpath '//*[@class="paragraph"]', output, 1
|
310
|
+
assert_xpath '//*[@class="paragraph"]/*[@class="title"][text() = "Title"]', output, 1
|
311
|
+
assert_xpath '//*[@class="paragraph"]/p[text() = "paragraph"]', output, 1
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context "Images" do
|
316
|
+
test "can render block image with alt text" do
|
317
|
+
input = <<-EOS
|
318
|
+
image::images/tiger.png[Tiger]
|
319
|
+
EOS
|
320
|
+
|
321
|
+
output = render_string input
|
322
|
+
assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1
|
323
|
+
end
|
324
|
+
|
325
|
+
test "can render block image with auto-generated alt text" do
|
326
|
+
input = <<-EOS
|
327
|
+
image::images/tiger.png[]
|
328
|
+
EOS
|
329
|
+
|
330
|
+
output = render_string input
|
331
|
+
assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="tiger"]', output, 1
|
332
|
+
end
|
333
|
+
|
334
|
+
test "can render block image with alt text and height and width" do
|
335
|
+
input = <<-EOS
|
336
|
+
image::images/tiger.png[Tiger, 200, 300]
|
337
|
+
EOS
|
338
|
+
|
339
|
+
output = render_string input
|
340
|
+
assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"][@width="200"][@height="300"]', output, 1
|
341
|
+
end
|
342
|
+
|
343
|
+
test "can render block image with link" do
|
344
|
+
input = <<-EOS
|
345
|
+
image::images/tiger.png[Tiger, link='http://en.wikipedia.org/wiki/Tiger']
|
346
|
+
EOS
|
347
|
+
|
348
|
+
output = render_string input
|
349
|
+
assert_xpath '//*[@class="imageblock"]//a[@class="image"][@href="http://en.wikipedia.org/wiki/Tiger"]/img[@src="images/tiger.png"][@alt="Tiger"]', output, 1
|
350
|
+
end
|
351
|
+
|
352
|
+
test "can render block image with caption" do
|
353
|
+
input = <<-EOS
|
354
|
+
.The AsciiDoc Tiger
|
355
|
+
image::images/tiger.png[Tiger]
|
356
|
+
EOS
|
357
|
+
|
358
|
+
output = render_string input
|
359
|
+
assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1
|
360
|
+
assert_xpath '//*[@class="imageblock"]/*[@class="title"][text() = "The AsciiDoc Tiger"]', output, 1
|
361
|
+
end
|
362
|
+
|
363
|
+
test 'can resolve image relative to imagesdir' do
|
364
|
+
input = <<-EOS
|
365
|
+
:imagesdir: images
|
366
|
+
|
367
|
+
image::tiger.png[Tiger]
|
368
|
+
EOS
|
369
|
+
|
370
|
+
output = render_string input
|
371
|
+
assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1
|
372
|
+
end
|
373
|
+
|
374
|
+
test 'embeds base64-encoded data uri for image when data-uri attribute is set' do
|
375
|
+
input = <<-EOS
|
376
|
+
:data-uri:
|
377
|
+
:imagesdir: fixtures
|
378
|
+
|
379
|
+
image::dot.gif[Dot]
|
380
|
+
EOS
|
381
|
+
|
382
|
+
doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
|
383
|
+
assert_equal 'fixtures', doc.attributes['imagesdir']
|
384
|
+
output = doc.render
|
385
|
+
assert_xpath '//*[@class="imageblock"]//img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Dot"]', output, 1
|
386
|
+
end
|
387
|
+
|
388
|
+
# this test will cause a warning to be printed to the console (until we have a message facility)
|
389
|
+
test 'cleans reference to ancestor directories before reading image if safe mode level is at least SAFE' do
|
390
|
+
input = <<-EOS
|
391
|
+
:data-uri:
|
392
|
+
:imagesdir: ../fixtures
|
393
|
+
|
394
|
+
image::dot.gif[Dot]
|
395
|
+
EOS
|
396
|
+
|
397
|
+
doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
|
398
|
+
assert_equal '../fixtures', doc.attributes['imagesdir']
|
399
|
+
output = doc.render
|
400
|
+
assert_xpath '//*[@class="imageblock"]//img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Dot"]', output, 1
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
context 'Admonition icons' do
|
405
|
+
test 'can resolve icon relative to default iconsdir' do
|
406
|
+
input = <<-EOS
|
407
|
+
:icons:
|
408
|
+
|
409
|
+
[TIP]
|
410
|
+
You can use icons for admonitions by setting the 'icons' attribute.
|
411
|
+
EOS
|
412
|
+
|
413
|
+
output = render_string input
|
414
|
+
assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="images/icons/tip.png"][@alt="Tip"]', output, 1
|
415
|
+
end
|
416
|
+
|
417
|
+
test 'can resolve icon relative to custom iconsdir' do
|
418
|
+
input = <<-EOS
|
419
|
+
:icons:
|
420
|
+
:iconsdir: icons
|
421
|
+
|
422
|
+
[TIP]
|
423
|
+
You can use icons for admonitions by setting the 'icons' attribute.
|
424
|
+
EOS
|
425
|
+
|
426
|
+
output = render_string input
|
427
|
+
assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="icons/tip.png"][@alt="Tip"]', output, 1
|
428
|
+
end
|
429
|
+
|
430
|
+
test 'embeds base64-encoded data uri of icon when data-uri attribute is set and safe mode level is less than SECURE' do
|
431
|
+
input = <<-EOS
|
432
|
+
:icons:
|
433
|
+
:iconsdir: fixtures
|
434
|
+
:iconstype: gif
|
435
|
+
:data-uri:
|
436
|
+
|
437
|
+
[TIP]
|
438
|
+
You can use icons for admonitions by setting the 'icons' attribute.
|
439
|
+
EOS
|
440
|
+
|
441
|
+
output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
|
442
|
+
assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Tip"]', output, 1
|
443
|
+
end
|
444
|
+
|
445
|
+
test 'does not embed base64-encoded data uri of icon when safe mode level is at least SECURE' do
|
446
|
+
input = <<-EOS
|
447
|
+
:icons:
|
448
|
+
:iconsdir: fixtures
|
449
|
+
:iconstype: gif
|
450
|
+
:data-uri:
|
451
|
+
|
452
|
+
[TIP]
|
453
|
+
You can use icons for admonitions by setting the 'icons' attribute.
|
454
|
+
EOS
|
455
|
+
|
456
|
+
output = render_string input
|
457
|
+
assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="fixtures/tip.gif"][@alt="Tip"]', output, 1
|
458
|
+
end
|
459
|
+
|
460
|
+
test 'cleans reference to ancestor directories before reading icon if safe mode level is at least SAFE' do
|
461
|
+
input = <<-EOS
|
462
|
+
:icons:
|
463
|
+
:iconsdir: ../fixtures
|
464
|
+
:iconstype: gif
|
465
|
+
:data-uri:
|
466
|
+
|
467
|
+
[TIP]
|
468
|
+
You can use icons for admonitions by setting the 'icons' attribute.
|
469
|
+
EOS
|
470
|
+
|
471
|
+
output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
|
472
|
+
assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Tip"]', output, 1
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
context 'Image paths' do
|
477
|
+
|
478
|
+
test 'restricts access to ancestor directories when safe mode level is at least SAFE' do
|
479
|
+
input = <<-EOS
|
480
|
+
image::asciidoctor.png[Asciidoctor]
|
481
|
+
EOS
|
482
|
+
basedir = File.dirname(__FILE__)
|
483
|
+
block = block_from_string input, :attributes => {'docdir' => basedir}
|
484
|
+
doc = block.document
|
485
|
+
assert doc.safe >= Asciidoctor::SafeMode::SAFE
|
486
|
+
|
487
|
+
assert_equal File.join(basedir, 'images'), block.normalize_asset_path('images')
|
488
|
+
assert_equal File.join(basedir, 'etc/images'), block.normalize_asset_path('/etc/images')
|
489
|
+
assert_equal File.join(basedir, 'images'), block.normalize_asset_path('../../images')
|
490
|
+
end
|
491
|
+
|
492
|
+
test 'does not restrict access to ancestor directories when safe mode is disabled' do
|
493
|
+
input = <<-EOS
|
494
|
+
image::asciidoctor.png[Asciidoctor]
|
495
|
+
EOS
|
496
|
+
basedir = File.dirname(__FILE__)
|
497
|
+
block = block_from_string input, :safe => Asciidoctor::SafeMode::UNSAFE, :attributes => {'docdir' => basedir}
|
498
|
+
doc = block.document
|
499
|
+
assert doc.safe == Asciidoctor::SafeMode::UNSAFE
|
500
|
+
|
501
|
+
assert_equal File.join(basedir, 'images'), block.normalize_asset_path('images')
|
502
|
+
assert_equal '/etc/images', block.normalize_asset_path('/etc/images')
|
503
|
+
assert_equal File.expand_path(File.join(basedir, '../../images')), block.normalize_asset_path('../../images')
|
504
|
+
end
|
505
|
+
|
506
|
+
end
|
507
|
+
|
508
|
+
context 'Source code' do
|
509
|
+
test 'should highlight source if source-highlighter attribute is coderay' do
|
510
|
+
input = <<-EOS
|
511
|
+
:source-highlighter: coderay
|
512
|
+
|
513
|
+
[source, ruby]
|
514
|
+
----
|
515
|
+
require 'coderay'
|
516
|
+
|
517
|
+
html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table)
|
518
|
+
----
|
519
|
+
EOS
|
520
|
+
output = render_string input, :safe => Asciidoctor::SafeMode::SAFE
|
521
|
+
assert_xpath '//pre[@class="highlight CodeRay"]/code[@class="ruby"]//span[@class = "constant"][text() = "CodeRay"]', output, 1
|
522
|
+
assert_match(/\.CodeRay \{/, output)
|
523
|
+
end
|
524
|
+
|
525
|
+
test 'should highlight source inline if source-highlighter attribute is coderay and coderay-css is style' do
|
526
|
+
input = <<-EOS
|
527
|
+
:source-highlighter: coderay
|
528
|
+
:coderay-css: style
|
529
|
+
|
530
|
+
[source, ruby]
|
531
|
+
----
|
532
|
+
require 'coderay'
|
533
|
+
|
534
|
+
html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table)
|
535
|
+
----
|
536
|
+
EOS
|
537
|
+
output = render_string input, :safe => Asciidoctor::SafeMode::SAFE
|
538
|
+
assert_xpath '//pre[@class="highlight CodeRay"]/code[@class="ruby"]//span[@style = "color:#036;font-weight:bold"][text() = "CodeRay"]', output, 1
|
539
|
+
assert_no_match(/\.CodeRay \{/, output)
|
540
|
+
end
|
541
|
+
|
542
|
+
test 'should include remote highlight.js assets if source-highlighter attribute is highlightjs' do
|
543
|
+
input = <<-EOS
|
544
|
+
:source-highlighter: highlightjs
|
545
|
+
|
546
|
+
[source, javascript]
|
547
|
+
----
|
548
|
+
<link rel="stylesheet" href="styles/default.css">
|
549
|
+
<script src="highlight.pack.js"></script>
|
550
|
+
<script>hljs.initHighlightingOnLoad();</script>
|
551
|
+
----
|
552
|
+
EOS
|
553
|
+
output = render_string input, :safe => Asciidoctor::SafeMode::SAFE
|
554
|
+
assert_match(/<link .*highlight\.js/, output)
|
555
|
+
assert_match(/<script .*highlight\.js/, output)
|
556
|
+
assert_match(/hljs.initHighlightingOnLoad/, output)
|
557
|
+
end
|
558
|
+
|
559
|
+
test 'document cannot turn on source highlighting if safe mode is at least SECURE' do
|
560
|
+
input = <<-EOS
|
561
|
+
:source-highlighter: coderay
|
562
|
+
EOS
|
563
|
+
doc = document_from_string input
|
564
|
+
assert doc.attributes['source-highlighter'].nil?
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
end
|