asciidoctor 1.5.6.2 → 1.5.7
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.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +330 -143
- data/README-fr.adoc +441 -0
- data/README-jp.adoc +418 -0
- data/README-zh_CN.adoc +430 -0
- data/README.adoc +454 -0
- data/Rakefile +57 -0
- data/asciidoctor.gemspec +7 -1
- data/data/locale/attributes-ar.adoc +22 -0
- data/data/locale/attributes-bg.adoc +22 -0
- data/data/locale/attributes-ca.adoc +22 -0
- data/data/locale/attributes-cs.adoc +22 -0
- data/data/locale/attributes-da.adoc +22 -0
- data/data/locale/attributes-de.adoc +22 -0
- data/data/locale/attributes-en.adoc +23 -0
- data/data/locale/attributes-es.adoc +22 -0
- data/data/locale/attributes-fa.adoc +22 -0
- data/data/locale/attributes-fi.adoc +22 -0
- data/data/locale/attributes-fr.adoc +22 -0
- data/data/locale/attributes-hu.adoc +22 -0
- data/data/locale/attributes-id.adoc +22 -0
- data/data/locale/attributes-it.adoc +22 -0
- data/data/locale/attributes-ja.adoc +22 -0
- data/data/locale/attributes-kr.adoc +22 -0
- data/data/locale/attributes-nb.adoc +22 -0
- data/data/locale/attributes-nl.adoc +22 -0
- data/data/locale/attributes-nn.adoc +22 -0
- data/data/locale/attributes-pl.adoc +22 -0
- data/data/locale/attributes-pt.adoc +22 -0
- data/data/locale/attributes-pt_BR.adoc +22 -0
- data/data/locale/attributes-ro.adoc +22 -0
- data/data/locale/attributes-ru.adoc +22 -0
- data/data/locale/attributes-sr.adoc +22 -0
- data/data/locale/attributes-sr_Latn.adoc +22 -0
- data/data/locale/attributes-tr.adoc +22 -0
- data/data/locale/attributes-uk.adoc +22 -0
- data/data/locale/attributes-zh_CN.adoc +22 -0
- data/data/locale/attributes-zh_TW.adoc +22 -0
- data/data/locale/attributes.adoc +8 -649
- data/data/stylesheets/asciidoctor-default.css +77 -72
- data/features/xref.feature +366 -7
- data/lib/asciidoctor.rb +107 -93
- data/lib/asciidoctor/abstract_block.rb +247 -239
- data/lib/asciidoctor/abstract_node.rb +56 -58
- data/lib/asciidoctor/block.rb +3 -3
- data/lib/asciidoctor/callouts.rb +1 -1
- data/lib/asciidoctor/cli/invoker.rb +36 -9
- data/lib/asciidoctor/cli/options.rb +63 -25
- data/lib/asciidoctor/converter.rb +23 -13
- data/lib/asciidoctor/converter/base.rb +4 -0
- data/lib/asciidoctor/converter/docbook45.rb +16 -9
- data/lib/asciidoctor/converter/docbook5.rb +115 -97
- data/lib/asciidoctor/converter/factory.rb +29 -31
- data/lib/asciidoctor/converter/html5.rb +229 -192
- data/lib/asciidoctor/converter/manpage.rb +72 -50
- data/lib/asciidoctor/converter/template.rb +12 -12
- data/lib/asciidoctor/core_ext.rb +5 -1
- data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +6 -0
- data/lib/asciidoctor/document.rb +168 -77
- data/lib/asciidoctor/extensions.rb +79 -47
- data/lib/asciidoctor/helpers.rb +33 -11
- data/lib/asciidoctor/inline.rb +3 -2
- data/lib/asciidoctor/list.rb +2 -1
- data/lib/asciidoctor/logging.rb +122 -0
- data/lib/asciidoctor/parser.rb +406 -382
- data/lib/asciidoctor/path_resolver.rb +169 -162
- data/lib/asciidoctor/reader.rb +166 -121
- data/lib/asciidoctor/section.rb +45 -28
- data/lib/asciidoctor/stylesheets.rb +13 -5
- data/lib/asciidoctor/substitutors.rb +328 -254
- data/lib/asciidoctor/table.rb +105 -48
- data/lib/asciidoctor/timings.rb +34 -6
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +41 -23
- data/man/asciidoctor.adoc +14 -8
- data/test/api_test.rb +1004 -0
- data/test/attributes_test.rb +241 -50
- data/test/blocks_test.rb +549 -124
- data/test/converter_test.rb +170 -78
- data/test/document_test.rb +208 -767
- data/test/extensions_test.rb +188 -53
- data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +1 -1
- data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +1 -1
- data/test/fixtures/file-with-missing-include.adoc +1 -0
- data/test/fixtures/include-file.jsx +8 -0
- data/test/fixtures/lists.adoc +96 -0
- data/test/fixtures/other-chapters.adoc +11 -0
- data/test/fixtures/outer-include.adoc +5 -0
- data/test/fixtures/sample.asciidoc +5 -1
- data/test/fixtures/subdir/index.adoc +3 -0
- data/test/fixtures/subdir/inner-include.adoc +3 -0
- data/test/fixtures/subdir/middle-include.adoc +5 -0
- data/test/fixtures/tagged-class-enclosed.rb +0 -1
- data/test/fixtures/unclosed-tag.adoc +3 -0
- data/test/fixtures/unexpected-end-tag.adoc +4 -0
- data/test/invoker_test.rb +101 -40
- data/test/links_test.rb +266 -72
- data/test/lists_test.rb +243 -45
- data/test/logger_test.rb +211 -0
- data/test/manpage_test.rb +124 -6
- data/test/options_test.rb +46 -1
- data/test/paragraphs_test.rb +23 -10
- data/test/parser_test.rb +30 -1
- data/test/paths_test.rb +115 -33
- data/test/preamble_test.rb +1 -1
- data/test/reader_test.rb +337 -81
- data/test/sections_test.rb +656 -72
- data/test/substitutions_test.rb +182 -57
- data/test/tables_test.rb +324 -57
- data/test/test_helper.rb +77 -32
- data/test/text_test.rb +7 -7
- metadata +67 -3
@@ -18,7 +18,7 @@ module Asciidoctor
|
|
18
18
|
LeadingPeriodRx = /^\./
|
19
19
|
EscapedMacroRx = /^(?:#{ESC}\\c\n)?#{ESC}\.((?:URL|MTO) ".*?" ".*?" )( |[^\s]*)(.*?)(?: *#{ESC}\\c)?$/
|
20
20
|
MockBoundaryRx = /<\/?BOUNDARY>/
|
21
|
-
EmDashCharRefRx = /—(
|
21
|
+
EmDashCharRefRx = /—(?:​)?/
|
22
22
|
EllipsisCharRefRx = /…(?:​)?/
|
23
23
|
|
24
24
|
# Converts HTML entity references back to their original form, escapes
|
@@ -57,6 +57,7 @@ module Asciidoctor
|
|
57
57
|
gsub('⇐', '\(lA'). # leftwards double arrow
|
58
58
|
gsub('⇒', '\(rA'). # rightwards double arrow
|
59
59
|
gsub('​', '\:'). # zero width space
|
60
|
+
gsub('&','&'). # literal ampersand (NOTE must take place after any other replacement that includes &)
|
60
61
|
gsub('\'', '\(aq'). # apostrophe-quote
|
61
62
|
gsub(MockBoundaryRx, ''). # mock boundary
|
62
63
|
gsub(ESC_BS, '\\'). # unescape troff backslash (NOTE update if more escapes are added)
|
@@ -66,7 +67,7 @@ module Asciidoctor
|
|
66
67
|
end
|
67
68
|
|
68
69
|
def skip_with_warning node, name = nil
|
69
|
-
warn %(
|
70
|
+
logger.warn %(converter missing for #{name || node.node_name} node in manpage backend)
|
70
71
|
nil
|
71
72
|
end
|
72
73
|
|
@@ -81,7 +82,7 @@ module Asciidoctor
|
|
81
82
|
# NOTE the first line enables the table (tbl) preprocessor, necessary for non-Linux systems
|
82
83
|
result = [%('\\" t
|
83
84
|
.\\" Title: #{mantitle}
|
84
|
-
.\\" Author: #{(node.attr? 'authors') ? (node.attr 'authors') : '[see the "
|
85
|
+
.\\" Author: #{(node.attr? 'authors') ? (node.attr 'authors') : '[see the "AUTHOR(S)" section]'}
|
85
86
|
.\\" Generator: Asciidoctor #{node.attr 'asciidoctor-version'})]
|
86
87
|
result << %(.\\" Date: #{docdate}) if docdate
|
87
88
|
result << %(.\\" Manual: #{(manual = node.attr 'manmanual') || '\ \&'}
|
@@ -105,22 +106,33 @@ module Asciidoctor
|
|
105
106
|
# define URL macro for portability
|
106
107
|
# see http://web.archive.org/web/20060102165607/http://people.debian.org/~branden/talks/wtfm/wtfm.pdf
|
107
108
|
#
|
108
|
-
#
|
109
|
+
# Usage
|
110
|
+
#
|
111
|
+
# .URL "http://www.debian.org" "Debian" "."
|
109
112
|
#
|
110
113
|
# * First argument: the URL
|
111
114
|
# * Second argument: text to be hyperlinked
|
112
|
-
# * Third (optional) argument: text that needs to immediately trail
|
113
|
-
# the hyperlink without intervening whitespace
|
115
|
+
# * Third (optional) argument: text that needs to immediately trail the hyperlink without intervening whitespace
|
114
116
|
result << '.de URL
|
115
|
-
\\\\$2
|
117
|
+
\\fI\\\\$2\\fP <\\\\$1>\\\\$3
|
116
118
|
..
|
117
|
-
.
|
118
|
-
|
119
|
+
.als MTO URL
|
120
|
+
.if \n[.g] \{\
|
121
|
+
. mso www.tmac
|
122
|
+
. am URL
|
123
|
+
. ad l
|
124
|
+
. .
|
125
|
+
. am MTO
|
126
|
+
. ad l
|
127
|
+
. .'
|
128
|
+
result << %(. LINKSTYLE #{node.attr 'man-linkstyle', 'blue R < >'})
|
129
|
+
result << '.\}'
|
119
130
|
|
120
131
|
unless node.noheader
|
121
132
|
if node.attr? 'manpurpose'
|
122
|
-
|
123
|
-
|
133
|
+
mannames = node.attr 'mannames', [manname]
|
134
|
+
result << %(.SH "#{(node.attr 'manname-title').upcase}"
|
135
|
+
#{mannames.map {|n| manify n } * ', '} \\- #{manify node.attr 'manpurpose'})
|
124
136
|
end
|
125
137
|
end
|
126
138
|
|
@@ -132,14 +144,19 @@ module Asciidoctor
|
|
132
144
|
result.concat(node.footnotes.map {|fn| %(#{fn.index}. #{fn.text}) })
|
133
145
|
end
|
134
146
|
|
135
|
-
# FIXME
|
136
|
-
if node.attr
|
137
|
-
|
147
|
+
# FIXME we really need an API that returns the authors as an array
|
148
|
+
if (num_authors = (node.attr 'authorcount') || 0) > 0
|
149
|
+
if num_authors == 1
|
150
|
+
result << %(.SH "AUTHOR"
|
138
151
|
.sp
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
.
|
152
|
+
#{node.attr 'author'})
|
153
|
+
else
|
154
|
+
result << '.SH "AUTHORS"'
|
155
|
+
(1.upto num_authors).each do |i|
|
156
|
+
result << %(.sp
|
157
|
+
#{node.attr "author_#{i}"})
|
158
|
+
end
|
159
|
+
end
|
143
160
|
end
|
144
161
|
|
145
162
|
result * LF
|
@@ -176,16 +193,14 @@ Author(s).
|
|
176
193
|
|
177
194
|
def admonition node
|
178
195
|
result = []
|
179
|
-
result << %(.if n
|
180
|
-
.sp
|
181
|
-
.\\}
|
196
|
+
result << %(.if n .sp
|
182
197
|
.RS 4
|
183
198
|
.it 1 an-trap
|
184
199
|
.nr an-no-space-flag 1
|
185
200
|
.nr an-break-flag 1
|
186
201
|
.br
|
187
202
|
.ps +1
|
188
|
-
.B #{node.attr 'textlabel'}#{node.title? ? "\\fP: #{manify node.title}" :
|
203
|
+
.B #{node.attr 'textlabel'}#{node.title? ? "\\fP: #{manify node.title}" : ''}
|
189
204
|
.ps -1
|
190
205
|
.br
|
191
206
|
#{resolve_content node}
|
@@ -216,10 +231,12 @@ r lw(\n(.lu*75u/100u).'
|
|
216
231
|
result * LF
|
217
232
|
end
|
218
233
|
|
219
|
-
# TODO implement title for dlist
|
220
234
|
# TODO implement horizontal (if it makes sense)
|
221
235
|
def dlist node
|
222
236
|
result = []
|
237
|
+
result << %(.sp
|
238
|
+
.B #{manify node.title}
|
239
|
+
.br) if node.title?
|
223
240
|
counter = 0
|
224
241
|
node.items.each do |terms, dd|
|
225
242
|
counter += 1
|
@@ -265,15 +282,11 @@ r lw(\n(.lu*75u/100u).'
|
|
265
282
|
.B #{manify node.captioned_title}
|
266
283
|
.br) if node.title?
|
267
284
|
result << %(.sp
|
268
|
-
.if n
|
269
|
-
.RS 4
|
270
|
-
.\\}
|
285
|
+
.if n .RS 4
|
271
286
|
.nf
|
272
287
|
#{manify node.content}
|
273
288
|
.fi
|
274
|
-
.if n
|
275
|
-
.RE
|
276
|
-
.\\})
|
289
|
+
.if n .RE)
|
277
290
|
result * LF
|
278
291
|
end
|
279
292
|
|
@@ -283,15 +296,11 @@ r lw(\n(.lu*75u/100u).'
|
|
283
296
|
.B #{manify node.title}
|
284
297
|
.br) if node.title?
|
285
298
|
result << %(.sp
|
286
|
-
.if n
|
287
|
-
.RS 4
|
288
|
-
.\\}
|
299
|
+
.if n .RS 4
|
289
300
|
.nf
|
290
301
|
#{manify node.content}
|
291
302
|
.fi
|
292
|
-
.if n
|
293
|
-
.RE
|
294
|
-
.\\})
|
303
|
+
.if n .RE)
|
295
304
|
result * LF
|
296
305
|
end
|
297
306
|
|
@@ -308,8 +317,8 @@ r lw(\n(.lu*75u/100u).'
|
|
308
317
|
\\h'-04' #{idx + 1}.\\h'+01'\\c
|
309
318
|
.\\}
|
310
319
|
.el \\{\\
|
311
|
-
.sp -1
|
312
|
-
.IP " #{idx + 1}." 4.2
|
320
|
+
. sp -1
|
321
|
+
. IP " #{idx + 1}." 4.2
|
313
322
|
.\\}
|
314
323
|
#{manify item.text})
|
315
324
|
result << item.content if item.blocks?
|
@@ -378,7 +387,7 @@ r lw(\n(.lu*75u/100u).'
|
|
378
387
|
def stem node
|
379
388
|
title_element = node.title? ? %(.sp
|
380
389
|
.B #{manify node.title}
|
381
|
-
.br) :
|
390
|
+
.br) : ''
|
382
391
|
open, close = BLOCK_MATH_DELIMITERS[node.style.to_sym]
|
383
392
|
|
384
393
|
unless ((equation = node.content).start_with? open) && (equation.end_with? close)
|
@@ -538,8 +547,8 @@ allbox tab(:);'
|
|
538
547
|
\\h'-04'\\(bu\\h'+03'\\c
|
539
548
|
.\\}
|
540
549
|
.el \\{\\
|
541
|
-
.sp -1
|
542
|
-
.IP \\(bu 2.3
|
550
|
+
. sp -1
|
551
|
+
. IP \\(bu 2.3
|
543
552
|
.\\}
|
544
553
|
#{manify item.text}]
|
545
554
|
result << item.content if item.blocks?
|
@@ -574,8 +583,8 @@ allbox tab(:);'
|
|
574
583
|
end
|
575
584
|
|
576
585
|
def video node
|
577
|
-
start_param = (node.attr? 'start', nil, false) ? %(&start=#{node.attr 'start'}) :
|
578
|
-
end_param = (node.attr? 'end', nil, false) ? %(&end=#{node.attr 'end'}) :
|
586
|
+
start_param = (node.attr? 'start', nil, false) ? %(&start=#{node.attr 'start'}) : ''
|
587
|
+
end_param = (node.attr? 'end', nil, false) ? %(&end=#{node.attr 'end'}) : ''
|
579
588
|
result = []
|
580
589
|
result << %(.sp
|
581
590
|
.B #{manify node.title}
|
@@ -588,17 +597,18 @@ allbox tab(:);'
|
|
588
597
|
target = node.target
|
589
598
|
case node.type
|
590
599
|
when :link
|
591
|
-
if (text = node.text) == target
|
592
|
-
text = nil
|
593
|
-
else
|
594
|
-
text = text.gsub '"', %[#{ESC_BS}(dq]
|
595
|
-
end
|
596
600
|
if target.start_with? 'mailto:'
|
597
601
|
macro = 'MTO'
|
598
|
-
target = target
|
602
|
+
target = target.slice 7, target.length
|
599
603
|
else
|
600
604
|
macro = 'URL'
|
601
605
|
end
|
606
|
+
if (text = node.text) == target
|
607
|
+
text = ''
|
608
|
+
else
|
609
|
+
text = text.gsub '"', %[#{ESC_BS}(dq]
|
610
|
+
end
|
611
|
+
target = target.sub '@', %[#{ESC_BS}(at] if macro == 'MTO'
|
602
612
|
%(#{ESC_BS}c#{LF}#{ESC_FS}#{macro} "#{target}" "#{text}" )
|
603
613
|
when :xref
|
604
614
|
refid = (node.attr 'refid') || target
|
@@ -607,7 +617,8 @@ allbox tab(:);'
|
|
607
617
|
# These are anchor points, which shouldn't be visible
|
608
618
|
''
|
609
619
|
else
|
610
|
-
warn %(
|
620
|
+
logger.warn %(unknown anchor type: #{node.type.inspect})
|
621
|
+
nil
|
611
622
|
end
|
612
623
|
end
|
613
624
|
|
@@ -669,7 +680,7 @@ allbox tab(:);'
|
|
669
680
|
when :strong
|
670
681
|
%(#{ESC_BS}fB<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
|
671
682
|
when :monospaced
|
672
|
-
%
|
683
|
+
%[#{ESC_BS}f(CR<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP]
|
673
684
|
when :single
|
674
685
|
%[#{ESC_BS}(oq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(cq]
|
675
686
|
when :double
|
@@ -682,5 +693,16 @@ allbox tab(:);'
|
|
682
693
|
def resolve_content node
|
683
694
|
node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content})
|
684
695
|
end
|
696
|
+
|
697
|
+
def write_alternate_pages mannames, manvolnum, target
|
698
|
+
if mannames && mannames.size > 1
|
699
|
+
mannames.shift
|
700
|
+
manvolext = %(.#{manvolnum})
|
701
|
+
dir, basename = ::File.split target
|
702
|
+
mannames.each do |manname|
|
703
|
+
::IO.write ::File.join(dir, %(#{manname}#{manvolext})), %(.so #{basename})
|
704
|
+
end
|
705
|
+
end
|
706
|
+
end
|
685
707
|
end
|
686
708
|
end
|
@@ -39,8 +39,6 @@ module Asciidoctor
|
|
39
39
|
@caches = { :scans => ::ThreadSafe::Cache.new, :templates => ::ThreadSafe::Cache.new }
|
40
40
|
rescue ::LoadError
|
41
41
|
@caches = { :scans => {}, :templates => {} }
|
42
|
-
# FIXME perhaps only warn if the cache option is enabled (meaning not disabled)?
|
43
|
-
warn 'asciidoctor: WARNING: gem \'thread_safe\' is not installed. This gem is recommended when using custom backend templates.'
|
44
42
|
end
|
45
43
|
|
46
44
|
def self.caches
|
@@ -77,6 +75,7 @@ module Asciidoctor
|
|
77
75
|
end
|
78
76
|
case opts[:template_cache]
|
79
77
|
when true
|
78
|
+
logger.warn 'gem \'thread_safe\' is not installed. This gem is recommended when using the built-in template cache.' unless defined? ::ThreadSafe
|
80
79
|
@caches = self.class.caches
|
81
80
|
when ::Hash
|
82
81
|
@caches = opts[:template_cache]
|
@@ -105,24 +104,25 @@ module Asciidoctor
|
|
105
104
|
engine = @engine
|
106
105
|
@template_dirs.each do |template_dir|
|
107
106
|
# FIXME need to think about safe mode restrictions here
|
108
|
-
next unless ::File.directory?(template_dir = (path_resolver.system_path template_dir
|
107
|
+
next unless ::File.directory?(template_dir = (path_resolver.system_path template_dir))
|
109
108
|
|
110
|
-
# NOTE last matching template wins for template name if no engine is given
|
111
|
-
file_pattern = '*'
|
112
109
|
if engine
|
113
110
|
file_pattern = %(*.#{engine})
|
114
111
|
# example: templates/haml
|
115
|
-
if ::File.directory?(engine_dir = (
|
112
|
+
if ::File.directory?(engine_dir = %(#{template_dir}/#{engine}))
|
116
113
|
template_dir = engine_dir
|
117
114
|
end
|
115
|
+
else
|
116
|
+
# NOTE last matching template wins for template name if no engine is given
|
117
|
+
file_pattern = '*'
|
118
118
|
end
|
119
119
|
|
120
|
-
# example: templates/html5 or templates/haml/html5
|
121
|
-
if ::File.directory?(backend_dir = (
|
120
|
+
# example: templates/html5 (engine not set) or templates/haml/html5 (engine set)
|
121
|
+
if ::File.directory?(backend_dir = %(#{template_dir}/#{backend}))
|
122
122
|
template_dir = backend_dir
|
123
123
|
end
|
124
124
|
|
125
|
-
pattern =
|
125
|
+
pattern = %(#{template_dir}/#{file_pattern})
|
126
126
|
|
127
127
|
if (scan_cache = @caches[:scans])
|
128
128
|
template_cache = @caches[:templates]
|
@@ -161,7 +161,7 @@ module Asciidoctor
|
|
161
161
|
end
|
162
162
|
else
|
163
163
|
metaclass.send :define_method, name do |node|
|
164
|
-
(template.render node).
|
164
|
+
(template.render node).rstrip
|
165
165
|
end
|
166
166
|
end
|
167
167
|
end
|
@@ -193,7 +193,7 @@ module Asciidoctor
|
|
193
193
|
if template_name == 'document'
|
194
194
|
(template.render node, opts).strip
|
195
195
|
else
|
196
|
-
(template.render node, opts).
|
196
|
+
(template.render node, opts).rstrip
|
197
197
|
end
|
198
198
|
end
|
199
199
|
|
@@ -280,7 +280,7 @@ module Asciidoctor
|
|
280
280
|
end
|
281
281
|
result[name] = template
|
282
282
|
end
|
283
|
-
if helpers || ::File.file?(helpers = (
|
283
|
+
if helpers || ::File.file?(helpers = %(#{template_dir}/helpers.rb))
|
284
284
|
require helpers
|
285
285
|
end
|
286
286
|
result
|
data/lib/asciidoctor/core_ext.rb
CHANGED
@@ -2,8 +2,12 @@ require 'asciidoctor/core_ext/nil_or_empty'
|
|
2
2
|
require 'asciidoctor/core_ext/regexp/is_match'
|
3
3
|
if RUBY_MIN_VERSION_1_9
|
4
4
|
require 'asciidoctor/core_ext/string/limit_bytesize'
|
5
|
-
|
5
|
+
if RUBY_ENGINE == 'opal'
|
6
|
+
require 'asciidoctor/core_ext/1.8.7/io/binread'
|
7
|
+
require 'asciidoctor/core_ext/1.8.7/io/write'
|
8
|
+
end
|
6
9
|
elsif RUBY_ENGINE != 'opal'
|
10
|
+
require 'asciidoctor/core_ext/1.8.7/base64/strict_encode64'
|
7
11
|
require 'asciidoctor/core_ext/1.8.7/hash/key'
|
8
12
|
require 'asciidoctor/core_ext/1.8.7/io/binread'
|
9
13
|
require 'asciidoctor/core_ext/1.8.7/io/write'
|
data/lib/asciidoctor/document.rb
CHANGED
@@ -1,22 +1,85 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
module Asciidoctor
|
3
|
-
# Public:
|
3
|
+
# Public: The Document class represents a parsed AsciiDoc document.
|
4
4
|
#
|
5
|
-
#
|
5
|
+
# Document is the root node of a parsed AsciiDoc document. It provides an
|
6
|
+
# abstract syntax tree (AST) that represents the structure of the AsciiDoc
|
7
|
+
# document from which the Document object was parsed.
|
6
8
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# first_section.title - title of first section in document, if present
|
13
|
-
# header.title - title of section level 0
|
9
|
+
# Although the constructor can be used to create an empty document object, more
|
10
|
+
# commonly, you'll load the document object from AsciiDoc source using the
|
11
|
+
# primary API methods, {Asciidoctor.load} or {Asciidoctor.load_file}. When
|
12
|
+
# using one of these APIs, you almost always want to set the safe mode to
|
13
|
+
# :safe (or :unsafe) to enable all of Asciidoctor's features.
|
14
14
|
#
|
15
|
-
#
|
15
|
+
# Asciidoctor.load '= Hello, AsciiDoc!', safe: :safe
|
16
|
+
# # => Asciidoctor::Document { doctype: "article", doctitle: "Hello, Asciidoc!", blocks: 0 }
|
16
17
|
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
18
|
+
# Instances of this class can be used to extract information from the document
|
19
|
+
# or alter its structure. As such, the Document object is most often used in
|
20
|
+
# extensions and by integrations.
|
21
|
+
#
|
22
|
+
# The most basic usage of the Document object is to retrieve the document's
|
23
|
+
# title.
|
24
|
+
#
|
25
|
+
# source = '= Document Title'
|
26
|
+
# document = Asciidoctor.load source, safe: :safe
|
27
|
+
# document.doctitle
|
28
|
+
# # => 'Document Title'
|
29
|
+
#
|
30
|
+
# If the document has no title, the {Document#doctitle} method returns the
|
31
|
+
# title of the first section. If that check falls through, you can have the
|
32
|
+
# method return a fallback value (the value of the untitled-label attribute).
|
33
|
+
#
|
34
|
+
# Asciidoctor.load('no doctitle', safe: :safe).doctitle use_fallback: true
|
35
|
+
# # => "Untitled"
|
36
|
+
#
|
37
|
+
# You can also use the Document object to access document attributes defined in
|
38
|
+
# the header, such as the author and doctype.
|
39
|
+
#
|
40
|
+
# source = '= Document Title
|
41
|
+
# Author Name
|
42
|
+
# :doctype: book'
|
43
|
+
# document = Asciidoctor.load source, safe: :safe
|
44
|
+
# document.author
|
45
|
+
# # => 'Author Name'
|
46
|
+
# document.doctype
|
47
|
+
# # => 'book'
|
48
|
+
#
|
49
|
+
# You can retrieve arbitrary document attributes defined in the header using
|
50
|
+
# {Document#attr} or check for the existence of one using {Document#attr?}:
|
51
|
+
#
|
52
|
+
# source = '= Asciidoctor
|
53
|
+
# :uri-project: https://asciidoctor.org'
|
54
|
+
# document = Asciidoctor.load source, safe: :safe
|
55
|
+
# document.attr 'uri-project'
|
56
|
+
# # => 'https://asciidoctor.org'
|
57
|
+
# document.attr? 'icons'
|
58
|
+
# # => false
|
59
|
+
#
|
60
|
+
# Starting at the Document object, you can begin walking the document tree using
|
61
|
+
# the {Document#blocks} method:
|
62
|
+
#
|
63
|
+
# source = 'paragraph contents
|
64
|
+
#
|
65
|
+
# [sidebar]
|
66
|
+
# sidebar contents'
|
67
|
+
# doc = Asciidoctor.load source, safe: :safe
|
68
|
+
# doc.blocks.map {|block| block.context }
|
69
|
+
# # => [:paragraph, :sidebar]
|
70
|
+
#
|
71
|
+
# You can discover block nodes at any depth in the tree using the
|
72
|
+
# {AbstractBlock#find_by} method.
|
73
|
+
#
|
74
|
+
# source = '****
|
75
|
+
# paragraph in sidebar
|
76
|
+
# ****'
|
77
|
+
# doc = Asciidoctor.load source, safe: :safe
|
78
|
+
# doc.find_by(context: :paragraph).map {|block| block.context }
|
79
|
+
# # => [:paragraph]
|
80
|
+
#
|
81
|
+
# Loading a document object is the first step in the conversion process. You
|
82
|
+
# can take the process to completion by calling the {Document#convert} method.
|
20
83
|
class Document < AbstractBlock
|
21
84
|
|
22
85
|
Footnote = ::Struct.new :index, :id, :text
|
@@ -134,9 +197,6 @@ class Document < AbstractBlock
|
|
134
197
|
# Public: Get the Hash of document counters
|
135
198
|
attr_reader :counters
|
136
199
|
|
137
|
-
# Public: Get the Hash of callouts
|
138
|
-
attr_reader :callouts
|
139
|
-
|
140
200
|
# Public: Get the level-0 Section
|
141
201
|
attr_reader :header
|
142
202
|
|
@@ -158,6 +218,9 @@ class Document < AbstractBlock
|
|
158
218
|
# Public: Get the Reader associated with this document
|
159
219
|
attr_reader :reader
|
160
220
|
|
221
|
+
# Public: Get/Set the PathResolver instance used to resolve paths in this Document.
|
222
|
+
attr_reader :path_resolver
|
223
|
+
|
161
224
|
# Public: Get the Converter associated with this document
|
162
225
|
attr_reader :converter
|
163
226
|
|
@@ -183,11 +246,11 @@ class Document < AbstractBlock
|
|
183
246
|
if (parent_doc = options.delete :parent)
|
184
247
|
@parent_document = parent_doc
|
185
248
|
options[:base_dir] ||= parent_doc.base_dir
|
249
|
+
options[:catalog_assets] = true if parent_doc.options[:catalog_assets]
|
186
250
|
@catalog = parent_doc.catalog.inject({}) do |accum, (key, table)|
|
187
251
|
accum[key] = (key == :footnotes ? [] : table)
|
188
252
|
accum
|
189
253
|
end
|
190
|
-
@callouts = parent_doc.callouts
|
191
254
|
# QUESTION should we support setting attribute in parent document from nested document?
|
192
255
|
# NOTE we must dup or else all the assignments to the overrides clobbers the real attributes
|
193
256
|
@attribute_overrides = attr_overrides = parent_doc.attributes.dup
|
@@ -199,6 +262,8 @@ class Document < AbstractBlock
|
|
199
262
|
@safe = parent_doc.safe
|
200
263
|
@attributes['compat-mode'] = '' if (@compat_mode = parent_doc.compat_mode)
|
201
264
|
@sourcemap = parent_doc.sourcemap
|
265
|
+
@timings = nil
|
266
|
+
@path_resolver = parent_doc.path_resolver
|
202
267
|
@converter = parent_doc.converter
|
203
268
|
initialize_extensions = false
|
204
269
|
@extensions = parent_doc.extensions
|
@@ -211,25 +276,33 @@ class Document < AbstractBlock
|
|
211
276
|
:links => [],
|
212
277
|
:images => [],
|
213
278
|
:indexterms => [],
|
214
|
-
:
|
279
|
+
:callouts => Callouts.new,
|
280
|
+
:includes => {},
|
215
281
|
}
|
216
|
-
@callouts = Callouts.new
|
217
282
|
# copy attributes map and normalize keys
|
218
283
|
# attribute overrides are attributes that can only be set from the commandline
|
219
284
|
# a direct assignment effectively makes the attribute a constant
|
220
285
|
# a nil value or name with leading or trailing ! will result in the attribute being unassigned
|
221
|
-
attr_overrides = {}
|
222
|
-
(options[:attributes] || {}).each do |key,
|
223
|
-
if key.
|
224
|
-
key
|
225
|
-
|
286
|
+
@attribute_overrides = attr_overrides = {}
|
287
|
+
(options[:attributes] || {}).each do |key, val|
|
288
|
+
if key.end_with? '@'
|
289
|
+
if key.start_with? '!'
|
290
|
+
key, val = (key.slice 1, key.length), false
|
291
|
+
elsif key.end_with? '!@'
|
292
|
+
key, val = (key.slice 0, key.length - 2), false
|
293
|
+
else
|
294
|
+
key, val = key.chop, %(#{val}@)
|
295
|
+
end
|
296
|
+
elsif key.start_with? '!'
|
297
|
+
key, val = (key.slice 1, key.length), val == '@' ? false : nil
|
226
298
|
elsif key.end_with? '!'
|
227
|
-
key = key.chop
|
228
|
-
value = nil
|
299
|
+
key, val = key.chop, val == '@' ? false : nil
|
229
300
|
end
|
230
|
-
attr_overrides[key.downcase] =
|
301
|
+
attr_overrides[key.downcase] = val
|
302
|
+
end
|
303
|
+
if (to_file = options[:to_file])
|
304
|
+
attr_overrides['outfilesuffix'] = ::File.extname to_file
|
231
305
|
end
|
232
|
-
@attribute_overrides = attr_overrides
|
233
306
|
# safely resolve the safe mode from const, int or string
|
234
307
|
if !(safe_mode = options[:safe])
|
235
308
|
@safe = SafeMode::SECURE
|
@@ -246,6 +319,8 @@ class Document < AbstractBlock
|
|
246
319
|
end
|
247
320
|
@compat_mode = attr_overrides.key? 'compat-mode'
|
248
321
|
@sourcemap = options[:sourcemap]
|
322
|
+
@timings = options.delete :timings
|
323
|
+
@path_resolver = PathResolver.new
|
249
324
|
@converter = nil
|
250
325
|
initialize_extensions = defined? ::Asciidoctor::Extensions
|
251
326
|
@extensions = nil # initialize furthur down
|
@@ -292,9 +367,8 @@ class Document < AbstractBlock
|
|
292
367
|
attrs['table-caption'] = 'Table'
|
293
368
|
attrs['toc-title'] = 'Table of Contents'
|
294
369
|
#attrs['preface-title'] = 'Preface'
|
295
|
-
attrs['manname-title'] = 'NAME'
|
296
370
|
attrs['section-refsig'] = 'Section'
|
297
|
-
|
371
|
+
attrs['part-refsig'] = 'Part'
|
298
372
|
attrs['chapter-refsig'] = 'Chapter'
|
299
373
|
attrs['appendix-caption'] = attrs['appendix-refsig'] = 'Appendix'
|
300
374
|
attrs['untitled-label'] = 'Untitled'
|
@@ -327,7 +401,7 @@ class Document < AbstractBlock
|
|
327
401
|
elsif attr_overrides['docdir']
|
328
402
|
@base_dir = attr_overrides['docdir']
|
329
403
|
else
|
330
|
-
#warn '
|
404
|
+
#logger.warn 'setting base_dir is recommended when working with string documents' unless nested?
|
331
405
|
@base_dir = attr_overrides['docdir'] = ::Dir.pwd
|
332
406
|
end
|
333
407
|
|
@@ -354,10 +428,8 @@ class Document < AbstractBlock
|
|
354
428
|
if @safe >= SafeMode::SECURE
|
355
429
|
attr_overrides['max-attribute-value-size'] = 4096 unless attr_overrides.key? 'max-attribute-value-size'
|
356
430
|
# assign linkcss (preventing css embedding) unless explicitly disabled from the commandline or API
|
357
|
-
#
|
358
|
-
|
359
|
-
attr_overrides['linkcss'] = ''
|
360
|
-
end
|
431
|
+
#attr_overrides['linkcss'] = (attr_overrides.fetch 'linkcss', '') || nil
|
432
|
+
attr_overrides['linkcss'] = '' unless attr_overrides.key? 'linkcss'
|
361
433
|
# restrict document from enabling icons
|
362
434
|
attr_overrides['icons'] ||= nil
|
363
435
|
end
|
@@ -367,18 +439,16 @@ class Document < AbstractBlock
|
|
367
439
|
@max_attribute_value_size = (size = (attr_overrides['max-attribute-value-size'] ||= nil)) ? size.to_i.abs : nil
|
368
440
|
|
369
441
|
attr_overrides.delete_if do |key, val|
|
370
|
-
|
371
|
-
|
372
|
-
if val.nil?
|
373
|
-
attrs.delete(key)
|
374
|
-
else
|
375
|
-
# a value ending in @ indicates this attribute does not override
|
376
|
-
# an attribute with the same key in the document souce
|
442
|
+
if val
|
443
|
+
# a value ending in @ allows document to override value
|
377
444
|
if ::String === val && (val.end_with? '@')
|
378
|
-
val = val.chop
|
379
|
-
verdict = true
|
445
|
+
val, verdict = val.chop, true
|
380
446
|
end
|
381
447
|
attrs[key] = val
|
448
|
+
else
|
449
|
+
# a nil or false value both unset the attribute; only a nil value locks it
|
450
|
+
attrs.delete key
|
451
|
+
verdict = val == false
|
382
452
|
end
|
383
453
|
verdict
|
384
454
|
end
|
@@ -393,6 +463,7 @@ class Document < AbstractBlock
|
|
393
463
|
# don't need to do the extra processing within our own document
|
394
464
|
# FIXME line info isn't reported correctly within include files in nested document
|
395
465
|
@reader = Reader.new data, options[:cursor]
|
466
|
+
@source_location = @reader.cursor if @sourcemap
|
396
467
|
|
397
468
|
# Now parse the lines in the reader into blocks
|
398
469
|
# Eagerly parse (for now) since a subdocument is not a publicly accessible object
|
@@ -441,24 +512,24 @@ class Document < AbstractBlock
|
|
441
512
|
|
442
513
|
# fallback directories
|
443
514
|
attrs['stylesdir'] ||= '.'
|
444
|
-
attrs['iconsdir'] ||=
|
515
|
+
attrs['iconsdir'] ||= %(#{attrs.fetch 'imagesdir', './images'}/icons)
|
445
516
|
|
446
517
|
if initialize_extensions
|
447
518
|
if (ext_registry = options[:extension_registry])
|
448
|
-
# QUESTION should we warn the value type of
|
449
|
-
|
519
|
+
# QUESTION should we warn if the value type of this option is not a registry
|
520
|
+
if Extensions::Registry === ext_registry || (::RUBY_ENGINE_JRUBY &&
|
450
521
|
::AsciidoctorJ::Extensions::ExtensionRegistry === ext_registry)
|
451
|
-
|
522
|
+
@extensions = ext_registry.activate self
|
452
523
|
end
|
453
524
|
elsif ::Proc === (ext_block = options[:extensions])
|
454
|
-
|
455
|
-
|
456
|
-
|
525
|
+
@extensions = Extensions.create(&ext_block).activate self
|
526
|
+
elsif !Extensions.groups.empty?
|
527
|
+
@extensions = Extensions::Registry.new.activate self
|
457
528
|
end
|
458
|
-
@extensions = ext_registry.activate self
|
459
529
|
end
|
460
530
|
|
461
531
|
@reader = PreprocessorReader.new self, data, (Reader::Cursor.new attrs['docfile'], @base_dir), :normalize => true
|
532
|
+
@source_location = @reader.cursor if @sourcemap
|
462
533
|
end
|
463
534
|
end
|
464
535
|
|
@@ -481,6 +552,7 @@ class Document < AbstractBlock
|
|
481
552
|
# create reader if data is provided (used when data is not known at the time the Document object is created)
|
482
553
|
if data
|
483
554
|
@reader = PreprocessorReader.new doc, data, (Reader::Cursor.new @attributes['docfile'], @base_dir), :normalize => true
|
555
|
+
@source_location = @reader.cursor if @sourcemap
|
484
556
|
end
|
485
557
|
|
486
558
|
if (exts = @parent_document ? nil : @extensions) && exts.preprocessors?
|
@@ -583,6 +655,10 @@ class Document < AbstractBlock
|
|
583
655
|
@catalog[:footnotes]
|
584
656
|
end
|
585
657
|
|
658
|
+
def callouts
|
659
|
+
@catalog[:callouts]
|
660
|
+
end
|
661
|
+
|
586
662
|
def nested?
|
587
663
|
@parent_document ? true : false
|
588
664
|
end
|
@@ -609,14 +685,24 @@ class Document < AbstractBlock
|
|
609
685
|
@attributes['basebackend'] == base
|
610
686
|
end
|
611
687
|
|
612
|
-
#
|
688
|
+
# Public: Return the doctitle as a String
|
689
|
+
#
|
690
|
+
# Returns the resolved doctitle as a [String] or nil if a doctitle cannot be resolved
|
613
691
|
def title
|
614
|
-
|
692
|
+
doctitle
|
615
693
|
end
|
616
694
|
|
695
|
+
# Public: Set the title on the document header
|
696
|
+
#
|
697
|
+
# Set the title of the document header to the specified value. If the header
|
698
|
+
# does not exist, it is first created.
|
699
|
+
#
|
700
|
+
# title - the String title to assign as the title of the document header
|
701
|
+
#
|
702
|
+
# Returns the new [String] title assigned to the document header
|
617
703
|
def title= title
|
618
704
|
unless (sect = @header)
|
619
|
-
(sect = (@header = Section.new self, 0
|
705
|
+
(sect = (@header = Section.new self, 0)).sectname = 'header'
|
620
706
|
end
|
621
707
|
sect.title = title
|
622
708
|
end
|
@@ -641,14 +727,12 @@ class Document < AbstractBlock
|
|
641
727
|
# Returns the resolved title as a [Title] if the :partition option is passed or a [String] if not
|
642
728
|
# or nil if no value can be resolved.
|
643
729
|
def doctitle opts = {}
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
val =
|
648
|
-
|
649
|
-
|
650
|
-
else
|
651
|
-
return
|
730
|
+
unless (val = @attributes['title'])
|
731
|
+
if (sect = first_section)
|
732
|
+
val = sect.title
|
733
|
+
elsif !(opts[:use_fallback] && (val = @attributes['untitled-label']))
|
734
|
+
return
|
735
|
+
end
|
652
736
|
end
|
653
737
|
|
654
738
|
if (separator = opts[:partition])
|
@@ -704,7 +788,7 @@ class Document < AbstractBlock
|
|
704
788
|
#
|
705
789
|
# Returns The parent Block
|
706
790
|
def << block
|
707
|
-
|
791
|
+
assign_numeral block if block.context == :section
|
708
792
|
super
|
709
793
|
end
|
710
794
|
|
@@ -800,9 +884,8 @@ class Document < AbstractBlock
|
|
800
884
|
|
801
885
|
# Internal: Restore the attributes to the previously saved state (attributes in header)
|
802
886
|
def restore_attributes
|
803
|
-
@callouts.rewind unless @parent_document
|
804
|
-
|
805
|
-
@attributes = @header_attributes
|
887
|
+
@catalog[:callouts].rewind unless @parent_document
|
888
|
+
@attributes.replace @header_attributes
|
806
889
|
end
|
807
890
|
|
808
891
|
# Internal: Delete any attributes stored for playback
|
@@ -914,7 +997,7 @@ class Document < AbstractBlock
|
|
914
997
|
current_backend, current_basebackend, current_doctype = @backend, (attrs = @attributes)['basebackend'], @doctype
|
915
998
|
if new_backend.start_with? 'xhtml'
|
916
999
|
attrs['htmlsyntax'] = 'xml'
|
917
|
-
new_backend = new_backend
|
1000
|
+
new_backend = new_backend.slice 1, new_backend.length
|
918
1001
|
elsif new_backend.start_with? 'html'
|
919
1002
|
attrs['htmlsyntax'] = 'html' unless attrs['htmlsyntax'] == 'xml'
|
920
1003
|
end
|
@@ -941,7 +1024,7 @@ class Document < AbstractBlock
|
|
941
1024
|
elsif @converter
|
942
1025
|
new_basebackend = new_backend.sub TrailingDigitsRx, ''
|
943
1026
|
if (new_outfilesuffix = DEFAULT_EXTENSIONS[new_basebackend])
|
944
|
-
new_filetype = new_outfilesuffix
|
1027
|
+
new_filetype = new_outfilesuffix.slice 1, new_outfilesuffix.length
|
945
1028
|
else
|
946
1029
|
new_outfilesuffix, new_basebackend, new_filetype = '.html', 'html', 'html'
|
947
1030
|
end
|
@@ -1006,12 +1089,13 @@ class Document < AbstractBlock
|
|
1006
1089
|
def create_converter
|
1007
1090
|
converter_opts = {}
|
1008
1091
|
converter_opts[:htmlsyntax] = @attributes['htmlsyntax']
|
1009
|
-
|
1010
|
-
|
1092
|
+
if (template_dir = @options[:template_dir])
|
1093
|
+
template_dirs = [template_dir]
|
1011
1094
|
elsif (template_dirs = @options[:template_dirs])
|
1012
|
-
|
1095
|
+
template_dirs = Array template_dirs
|
1013
1096
|
end
|
1014
1097
|
if template_dirs
|
1098
|
+
converter_opts[:template_dirs] = template_dirs
|
1015
1099
|
converter_opts[:template_cache] = @options.fetch :template_cache, true
|
1016
1100
|
converter_opts[:template_engine] = @options[:template_engine]
|
1017
1101
|
converter_opts[:template_engine_options] = @options[:template_engine_options]
|
@@ -1033,9 +1117,8 @@ class Document < AbstractBlock
|
|
1033
1117
|
# loaded by the Converter. If a :template_dir is not specified,
|
1034
1118
|
# or a template is missing, the converter will fall back to
|
1035
1119
|
# using the appropriate built-in template.
|
1036
|
-
#--
|
1037
|
-
# QUESTION should we dup @header_attributes before converting?
|
1038
1120
|
def convert opts = {}
|
1121
|
+
@timings.start :convert if @timings
|
1039
1122
|
parse unless @parsed
|
1040
1123
|
unless @safe >= SafeMode::SERVER || opts.empty?
|
1041
1124
|
# QUESTION should we store these on the Document object?
|
@@ -1046,9 +1129,9 @@ class Document < AbstractBlock
|
|
1046
1129
|
# QUESTION should we add extensions that execute before conversion begins?
|
1047
1130
|
|
1048
1131
|
if doctype == 'inline'
|
1049
|
-
if (block = @blocks[0])
|
1132
|
+
if (block = @blocks[0] || @header)
|
1050
1133
|
if block.content_model == :compound || block.content_model == :empty
|
1051
|
-
warn
|
1134
|
+
logger.warn 'no inline candidate; use the inline doctype to convert a single paragragh, verbatim, or raw block'
|
1052
1135
|
else
|
1053
1136
|
output = block.content
|
1054
1137
|
end
|
@@ -1066,6 +1149,7 @@ class Document < AbstractBlock
|
|
1066
1149
|
end
|
1067
1150
|
end
|
1068
1151
|
|
1152
|
+
@timings.record :convert if @timings
|
1069
1153
|
output
|
1070
1154
|
end
|
1071
1155
|
|
@@ -1076,7 +1160,10 @@ class Document < AbstractBlock
|
|
1076
1160
|
#
|
1077
1161
|
# If the converter responds to :write, delegate the work of writing the file
|
1078
1162
|
# to that method. Otherwise, write the output the specified file.
|
1163
|
+
#
|
1164
|
+
# Returns nothing
|
1079
1165
|
def write output, target
|
1166
|
+
@timings.start :write if @timings
|
1080
1167
|
if Writer === @converter
|
1081
1168
|
@converter.write output, target
|
1082
1169
|
else
|
@@ -1089,8 +1176,12 @@ class Document < AbstractBlock
|
|
1089
1176
|
else
|
1090
1177
|
::IO.write target, output
|
1091
1178
|
end
|
1092
|
-
|
1179
|
+
if @backend == 'manpage' && ::String === target && (@converter.respond_to? :write_alternate_pages)
|
1180
|
+
@converter.write_alternate_pages @attributes['mannames'], @attributes['manvolnum'], target
|
1181
|
+
end
|
1093
1182
|
end
|
1183
|
+
@timings.record :write if @timings
|
1184
|
+
nil
|
1094
1185
|
end
|
1095
1186
|
|
1096
1187
|
=begin
|