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.

Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +330 -143
  3. data/README-fr.adoc +441 -0
  4. data/README-jp.adoc +418 -0
  5. data/README-zh_CN.adoc +430 -0
  6. data/README.adoc +454 -0
  7. data/Rakefile +57 -0
  8. data/asciidoctor.gemspec +7 -1
  9. data/data/locale/attributes-ar.adoc +22 -0
  10. data/data/locale/attributes-bg.adoc +22 -0
  11. data/data/locale/attributes-ca.adoc +22 -0
  12. data/data/locale/attributes-cs.adoc +22 -0
  13. data/data/locale/attributes-da.adoc +22 -0
  14. data/data/locale/attributes-de.adoc +22 -0
  15. data/data/locale/attributes-en.adoc +23 -0
  16. data/data/locale/attributes-es.adoc +22 -0
  17. data/data/locale/attributes-fa.adoc +22 -0
  18. data/data/locale/attributes-fi.adoc +22 -0
  19. data/data/locale/attributes-fr.adoc +22 -0
  20. data/data/locale/attributes-hu.adoc +22 -0
  21. data/data/locale/attributes-id.adoc +22 -0
  22. data/data/locale/attributes-it.adoc +22 -0
  23. data/data/locale/attributes-ja.adoc +22 -0
  24. data/data/locale/attributes-kr.adoc +22 -0
  25. data/data/locale/attributes-nb.adoc +22 -0
  26. data/data/locale/attributes-nl.adoc +22 -0
  27. data/data/locale/attributes-nn.adoc +22 -0
  28. data/data/locale/attributes-pl.adoc +22 -0
  29. data/data/locale/attributes-pt.adoc +22 -0
  30. data/data/locale/attributes-pt_BR.adoc +22 -0
  31. data/data/locale/attributes-ro.adoc +22 -0
  32. data/data/locale/attributes-ru.adoc +22 -0
  33. data/data/locale/attributes-sr.adoc +22 -0
  34. data/data/locale/attributes-sr_Latn.adoc +22 -0
  35. data/data/locale/attributes-tr.adoc +22 -0
  36. data/data/locale/attributes-uk.adoc +22 -0
  37. data/data/locale/attributes-zh_CN.adoc +22 -0
  38. data/data/locale/attributes-zh_TW.adoc +22 -0
  39. data/data/locale/attributes.adoc +8 -649
  40. data/data/stylesheets/asciidoctor-default.css +77 -72
  41. data/features/xref.feature +366 -7
  42. data/lib/asciidoctor.rb +107 -93
  43. data/lib/asciidoctor/abstract_block.rb +247 -239
  44. data/lib/asciidoctor/abstract_node.rb +56 -58
  45. data/lib/asciidoctor/block.rb +3 -3
  46. data/lib/asciidoctor/callouts.rb +1 -1
  47. data/lib/asciidoctor/cli/invoker.rb +36 -9
  48. data/lib/asciidoctor/cli/options.rb +63 -25
  49. data/lib/asciidoctor/converter.rb +23 -13
  50. data/lib/asciidoctor/converter/base.rb +4 -0
  51. data/lib/asciidoctor/converter/docbook45.rb +16 -9
  52. data/lib/asciidoctor/converter/docbook5.rb +115 -97
  53. data/lib/asciidoctor/converter/factory.rb +29 -31
  54. data/lib/asciidoctor/converter/html5.rb +229 -192
  55. data/lib/asciidoctor/converter/manpage.rb +72 -50
  56. data/lib/asciidoctor/converter/template.rb +12 -12
  57. data/lib/asciidoctor/core_ext.rb +5 -1
  58. data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +6 -0
  59. data/lib/asciidoctor/document.rb +168 -77
  60. data/lib/asciidoctor/extensions.rb +79 -47
  61. data/lib/asciidoctor/helpers.rb +33 -11
  62. data/lib/asciidoctor/inline.rb +3 -2
  63. data/lib/asciidoctor/list.rb +2 -1
  64. data/lib/asciidoctor/logging.rb +122 -0
  65. data/lib/asciidoctor/parser.rb +406 -382
  66. data/lib/asciidoctor/path_resolver.rb +169 -162
  67. data/lib/asciidoctor/reader.rb +166 -121
  68. data/lib/asciidoctor/section.rb +45 -28
  69. data/lib/asciidoctor/stylesheets.rb +13 -5
  70. data/lib/asciidoctor/substitutors.rb +328 -254
  71. data/lib/asciidoctor/table.rb +105 -48
  72. data/lib/asciidoctor/timings.rb +34 -6
  73. data/lib/asciidoctor/version.rb +1 -1
  74. data/man/asciidoctor.1 +41 -23
  75. data/man/asciidoctor.adoc +14 -8
  76. data/test/api_test.rb +1004 -0
  77. data/test/attributes_test.rb +241 -50
  78. data/test/blocks_test.rb +549 -124
  79. data/test/converter_test.rb +170 -78
  80. data/test/document_test.rb +208 -767
  81. data/test/extensions_test.rb +188 -53
  82. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +1 -1
  83. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +1 -1
  84. data/test/fixtures/file-with-missing-include.adoc +1 -0
  85. data/test/fixtures/include-file.jsx +8 -0
  86. data/test/fixtures/lists.adoc +96 -0
  87. data/test/fixtures/other-chapters.adoc +11 -0
  88. data/test/fixtures/outer-include.adoc +5 -0
  89. data/test/fixtures/sample.asciidoc +5 -1
  90. data/test/fixtures/subdir/index.adoc +3 -0
  91. data/test/fixtures/subdir/inner-include.adoc +3 -0
  92. data/test/fixtures/subdir/middle-include.adoc +5 -0
  93. data/test/fixtures/tagged-class-enclosed.rb +0 -1
  94. data/test/fixtures/unclosed-tag.adoc +3 -0
  95. data/test/fixtures/unexpected-end-tag.adoc +4 -0
  96. data/test/invoker_test.rb +101 -40
  97. data/test/links_test.rb +266 -72
  98. data/test/lists_test.rb +243 -45
  99. data/test/logger_test.rb +211 -0
  100. data/test/manpage_test.rb +124 -6
  101. data/test/options_test.rb +46 -1
  102. data/test/paragraphs_test.rb +23 -10
  103. data/test/parser_test.rb +30 -1
  104. data/test/paths_test.rb +115 -33
  105. data/test/preamble_test.rb +1 -1
  106. data/test/reader_test.rb +337 -81
  107. data/test/sections_test.rb +656 -72
  108. data/test/substitutions_test.rb +182 -57
  109. data/test/tables_test.rb +324 -57
  110. data/test/test_helper.rb +77 -32
  111. data/test/text_test.rb +7 -7
  112. 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 = /&#8212(?:;&#8203;)?/
21
+ EmDashCharRefRx = /&#8212;(?:&#8203;)?/
22
22
  EllipsisCharRefRx = /&#8230;(?:&#8203;)?/
23
23
 
24
24
  # Converts HTML entity references back to their original form, escapes
@@ -57,6 +57,7 @@ module Asciidoctor
57
57
  gsub('&#8656;', '\(lA'). # leftwards double arrow
58
58
  gsub('&#8658;', '\(rA'). # rightwards double arrow
59
59
  gsub('&#8203;', '\:'). # zero width space
60
+ gsub('&amp;','&'). # 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 %(asciidoctor: WARNING: converter missing for #{name || node.node_name} node in manpage backend)
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 "AUTHORS" section]'}
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
- # Use: .URL "http://www.debian.org" "Debian" "."
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 \(laURL: \\\\$1 \(ra\\\\$3
117
+ \\fI\\\\$2\\fP <\\\\$1>\\\\$3
116
118
  ..
117
- .if \n[.g] .mso www.tmac'
118
- result << %(.LINKSTYLE #{node.attr 'man-linkstyle', 'blue R < >'})
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
- result << %(.SH "#{node.attr 'manname-title'}"
123
- #{manify mantitle} \\- #{manify node.attr 'manpurpose'})
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 detect single author and use appropriate heading; itemize the authors if multiple
136
- if node.attr? 'authors'
137
- result << %(.SH "AUTHOR(S)"
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
- \\fB#{node.attr 'authors'}\\fP
140
- .RS 4
141
- Author(s).
142
- .RE)
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}" : nil}
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) : nil
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'}) : nil
578
- end_param = (node.attr? 'end', nil, false) ? %(&end=#{node.attr 'end'}) : nil
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[7..-1].sub '@', %[#{ESC_BS}(at]
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 %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
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
- %(#{ESC_BS}f[CR]<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
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, nil))
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 = (::File.join template_dir, engine))
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 = (::File.join template_dir, backend))
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 = ::File.join template_dir, file_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).chomp
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).chomp
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 = (::File.join template_dir, 'helpers.rb'))
283
+ if helpers || ::File.file?(helpers = %(#{template_dir}/helpers.rb))
284
284
  require helpers
285
285
  end
286
286
  result
@@ -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
- require 'asciidoctor/core_ext/1.8.7/io/write' if RUBY_ENGINE == 'opal'
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'
@@ -0,0 +1,6 @@
1
+ # Educate Ruby 1.8.7 about the Base64#strict_encode64 method.
2
+ module Base64
3
+ def strict_encode64 bin
4
+ (self.encode64 bin).delete %(\n)
5
+ end
6
+ end
@@ -1,22 +1,85 @@
1
1
  # encoding: UTF-8
2
2
  module Asciidoctor
3
- # Public: Methods for parsing and converting AsciiDoc documents.
3
+ # Public: The Document class represents a parsed AsciiDoc document.
4
4
  #
5
- # There are several strategies for getting the title of the document:
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
- # doctitle - value of title attribute, if assigned and non-empty,
8
- # otherwise title of first section in document, if present
9
- # otherwise nil
10
- # name - an alias of doctitle
11
- # title - value of the title attribute, or nil if not present
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
- # Keep in mind that you'll want to honor these document settings:
15
+ # Asciidoctor.load '= Hello, AsciiDoc!', safe: :safe
16
+ # # => Asciidoctor::Document { doctype: "article", doctitle: "Hello, Asciidoc!", blocks: 0 }
16
17
  #
17
- # notitle - The h1 heading should not be shown
18
- # noheader - The header block (h1 heading, author, revision info) should not be shown
19
- # nofooter - the footer block should not be shown
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
- :includes => ::Set.new,
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, value|
223
- if key.start_with? '!'
224
- key = key[1..-1]
225
- value = nil
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] = value
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
- #attrs['part-refsig'] = 'Part'
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 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested?
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
- # effectively the same has "has key 'linkcss' and value == nil"
358
- unless attr_overrides.fetch('linkcss', '').nil?
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
- verdict = false
371
- # a nil value undefines the attribute
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'] ||= ::File.join(attrs.fetch('imagesdir', './images'), 'icons')
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 the option is not a registry or boolean?
449
- unless Extensions::Registry === ext_registry || (::RUBY_ENGINE_JRUBY &&
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
- ext_registry = Extensions::Registry.new
522
+ @extensions = ext_registry.activate self
452
523
  end
453
524
  elsif ::Proc === (ext_block = options[:extensions])
454
- ext_registry = Extensions.create(&ext_block)
455
- else
456
- ext_registry = Extensions::Registry.new
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
- # The title explicitly defined in the document attributes
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
- @attributes['title']
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, false)).sectname = 'header'
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
- if !(val = @attributes['title'].nil_or_empty?)
645
- val = title
646
- elsif (sect = first_section)
647
- val = sect.title
648
- elsif opts[:use_fallback] && (val = @attributes['untitled-label'])
649
- # use val set in condition
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
- enumerate_section block if block.context == :section
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
- # QUESTION shouldn't this be a dup in case we convert again?
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[1..-1]
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[1..-1]
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
- template_dirs = if (template_dir = @options[:template_dir])
1010
- converter_opts[:template_dirs] = [template_dir]
1092
+ if (template_dir = @options[:template_dir])
1093
+ template_dirs = [template_dir]
1011
1094
  elsif (template_dirs = @options[:template_dirs])
1012
- converter_opts[:template_dirs] = template_dirs
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 %(asciidoctor: WARNING: no inline candidate; use the inline doctype to convert a single paragragh, verbatim, or raw block)
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
- nil
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