asciidoctor 0.1.1 → 0.1.2

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +1 -1
  3. data/LICENSE +2 -2
  4. data/README.adoc +461 -0
  5. data/asciidoctor.gemspec +27 -16
  6. data/compat/asciidoc.conf +139 -0
  7. data/lib/asciidoctor.rb +212 -69
  8. data/lib/asciidoctor/abstract_block.rb +41 -0
  9. data/lib/asciidoctor/abstract_node.rb +128 -81
  10. data/lib/asciidoctor/attribute_list.rb +5 -2
  11. data/lib/asciidoctor/backends/base_template.rb +16 -4
  12. data/lib/asciidoctor/backends/docbook45.rb +112 -42
  13. data/lib/asciidoctor/backends/html5.rb +206 -90
  14. data/lib/asciidoctor/block.rb +5 -5
  15. data/lib/asciidoctor/cli/invoker.rb +38 -34
  16. data/lib/asciidoctor/cli/options.rb +3 -3
  17. data/lib/asciidoctor/document.rb +115 -13
  18. data/lib/asciidoctor/helpers.rb +16 -0
  19. data/lib/asciidoctor/lexer.rb +486 -359
  20. data/lib/asciidoctor/path_resolver.rb +360 -0
  21. data/lib/asciidoctor/reader.rb +122 -23
  22. data/lib/asciidoctor/renderer.rb +1 -33
  23. data/lib/asciidoctor/section.rb +1 -1
  24. data/lib/asciidoctor/substituters.rb +103 -19
  25. data/lib/asciidoctor/version.rb +1 -1
  26. data/man/asciidoctor.1 +6 -6
  27. data/man/asciidoctor.ad +5 -3
  28. data/stylesheets/asciidoctor.css +274 -0
  29. data/test/attributes_test.rb +133 -10
  30. data/test/blocks_test.rb +302 -17
  31. data/test/document_test.rb +269 -6
  32. data/test/fixtures/basic-docinfo.html +1 -0
  33. data/test/fixtures/basic-docinfo.xml +4 -0
  34. data/test/fixtures/basic.asciidoc +4 -0
  35. data/test/fixtures/docinfo.html +1 -0
  36. data/test/fixtures/docinfo.xml +2 -0
  37. data/test/fixtures/include-file.asciidoc +22 -1
  38. data/test/fixtures/stylesheets/custom.css +3 -0
  39. data/test/invoker_test.rb +38 -6
  40. data/test/lexer_test.rb +64 -21
  41. data/test/links_test.rb +4 -0
  42. data/test/lists_test.rb +251 -12
  43. data/test/paragraphs_test.rb +225 -30
  44. data/test/paths_test.rb +174 -0
  45. data/test/reader_test.rb +89 -2
  46. data/test/sections_test.rb +518 -16
  47. data/test/substitutions_test.rb +121 -10
  48. data/test/tables_test.rb +53 -13
  49. data/test/test_helper.rb +2 -2
  50. data/test/text_test.rb +5 -5
  51. metadata +46 -50
  52. data/README.asciidoc +0 -296
  53. data/lib/asciidoctor/errors.rb +0 -5
@@ -26,6 +26,7 @@ class Block < AbstractBlock
26
26
  def initialize(parent, context, buffer = nil)
27
27
  super(parent, context)
28
28
  @buffer = buffer
29
+ @caption = nil
29
30
  end
30
31
 
31
32
  # Public: Get the rendered String content for this Block. If the block
@@ -94,9 +95,8 @@ class Block < AbstractBlock
94
95
  # block.content
95
96
  # => ["<em>This</em> is what happens when you &lt;meet&gt; a stranger in the &lt;alps&gt;!"]
96
97
  def content
97
-
98
98
  case @context
99
- when :preamble, :open, :example, :sidebar
99
+ when :preamble, :open
100
100
  @blocks.map {|b| b.render }.join
101
101
  # lists get iterated in the template (for now)
102
102
  # list items recurse into this block when their text and content methods are called
@@ -106,14 +106,14 @@ class Block < AbstractBlock
106
106
  apply_literal_subs(@buffer)
107
107
  when :pass
108
108
  apply_passthrough_subs(@buffer)
109
- when :quote, :verse, :admonition
109
+ when :admonition, :example, :sidebar, :quote, :verse
110
110
  if !@buffer.nil?
111
- apply_normal_subs(@buffer)
111
+ apply_para_subs(@buffer)
112
112
  else
113
113
  @blocks.map {|b| b.render }.join
114
114
  end
115
115
  else
116
- apply_normal_subs(@buffer)
116
+ apply_para_subs(@buffer)
117
117
  end
118
118
  end
119
119
 
@@ -5,14 +5,12 @@ module Asciidoctor
5
5
  attr_reader :options
6
6
  attr_reader :document
7
7
  attr_reader :code
8
- attr_reader :timings
9
8
 
10
9
  def initialize(*options)
11
10
  @document = nil
12
11
  @out = nil
13
12
  @err = nil
14
13
  @code = 0
15
- @timings = {}
16
14
  options = options.flatten
17
15
  if !options.empty? && options.first.is_a?(Cli::Options)
18
16
  @options = options.first
@@ -32,46 +30,52 @@ module Asciidoctor
32
30
  return if @options.nil?
33
31
 
34
32
  begin
35
- @timings = {}
36
- infile = @options[:input_file]
37
- outfile = @options[:output_file]
33
+ opts = {}
34
+ monitor = {}
35
+ infile = nil
36
+ outfile = nil
37
+ @options.map {|k, v|
38
+ case k
39
+ when :input_file
40
+ infile = v
41
+ when :output_file
42
+ outfile = v
43
+ when :destination_dir
44
+ #opts[:to_dir] = File.expand_path(v) unless v.nil?
45
+ opts[:to_dir] = v unless v.nil?
46
+ when :attributes
47
+ opts[:attributes] = v.dup
48
+ when :verbose
49
+ opts[:monitor] = monitor if v
50
+ when :trace
51
+ # currently, nothing
52
+ else
53
+ opts[k] = v unless v.nil?
54
+ end
55
+ }
56
+
38
57
  if infile == '-'
39
58
  # allow use of block to supply stdin, particularly useful for tests
40
59
  input = block_given? ? yield : STDIN
41
60
  else
42
61
  input = File.new(infile)
43
62
  end
44
- start = Time.now
45
- @document = Asciidoctor.load(input, @options)
46
- timings[:parse] = Time.now - start
47
- start = Time.now
48
- output = @document.render
49
- timings[:render] = Time.now - start
50
- if @options[:verbose]
51
- puts "Time to read and parse source: #{timings[:parse]}"
52
- puts "Time to render document: #{timings[:render]}"
53
- puts "Total time to read, parse and render: #{timings.reduce(0) {|sum, (_, v)| sum += v}}"
54
- end
55
- if outfile == '/dev/null'
56
- # output nothing
57
- elsif outfile == '-' || (infile == '-' && (outfile.nil? || outfile.empty?))
58
- (@out || $stdout).puts output
63
+
64
+ if outfile == '-' || (infile == '-' && (outfile.to_s.empty? || outfile != '/dev/null'))
65
+ opts[:to_file] = (@out || $stdout)
66
+ elsif !outfile.nil?
67
+ opts[:to_file] = outfile
59
68
  else
60
- if outfile.nil? || outfile.empty?
61
- if @options[:destination_dir]
62
- destination_dir = File.expand_path(@options[:destination_dir])
63
- else
64
- destination_dir = @document.base_dir
65
- end
66
- outfile = File.join(destination_dir, "#{@document.attributes['docname']}#{@document.attributes['outfilesuffix']}")
67
- else
68
- outfile = @document.normalize_asset_path outfile
69
- end
69
+ opts[:in_place] = true unless opts.has_key? :to_dir
70
+ end
70
71
 
71
- # this assignment is primarily for testing or other post analysis
72
- @document.attributes['outfile'] = outfile
73
- @document.attributes['outdir'] = File.dirname(outfile)
74
- File.open(outfile, 'w') {|file| file.write output }
72
+ @document = Asciidoctor.render(input, opts)
73
+
74
+ # FIXME this should be :monitor, :profile or :timings rather than :verbose
75
+ if @options[:verbose]
76
+ puts "Time to read and parse source: #{'%05.5f' % monitor[:parse]}"
77
+ puts "Time to render document: #{'%05.5f' % monitor[:render]}"
78
+ puts "Total time to read, parse and render: #{'%05.5f' % monitor[:load_render]}"
75
79
  end
76
80
  rescue Exception => e
77
81
  raise e if @options[:trace] || SystemExit === e
@@ -22,7 +22,7 @@ module Asciidoctor
22
22
  self[:eruby] = options[:eruby] || nil
23
23
  self[:compact] = options[:compact] || false
24
24
  self[:verbose] = options[:verbose] || false
25
- self[:base_dir] = options[:base_dir] || nil
25
+ self[:base_dir] = options[:base_dir]
26
26
  self[:destination_dir] = options[:destination_dir] || nil
27
27
  self[:trace] = false
28
28
  end
@@ -125,8 +125,8 @@ Example: asciidoctor -b html5 source.asciidoc
125
125
  if self[:input_file].nil? || self[:input_file].empty?
126
126
  $stderr.puts opts_parser
127
127
  return 1
128
- elsif self[:input_file] != '-' && !File.exist?(self[:input_file])
129
- $stderr.puts "asciidoctor: FAILED: input file #{self[:input_file]} missing"
128
+ elsif self[:input_file] != '-' && !File.readable?(self[:input_file])
129
+ $stderr.puts "asciidoctor: FAILED: input file #{self[:input_file]} missing or cannot be read"
130
130
  return 1
131
131
  end
132
132
  rescue OptionParser::MissingArgument
@@ -127,11 +127,12 @@ class Document < AbstractBlock
127
127
  @safe = @options.fetch(:safe, SafeMode::SECURE).to_i
128
128
  @options[:header_footer] = @options.fetch(:header_footer, false)
129
129
 
130
- @attributes['asciidoctor'] = ''
131
- @attributes['asciidoctor-version'] = VERSION
132
- @attributes['sectids'] = ''
133
130
  @attributes['encoding'] = 'UTF-8'
134
- @attributes['notitle'] = '' if !@options[:header_footer]
131
+ @attributes['sectids'] = ''
132
+ @attributes['notitle'] = '' unless @options[:header_footer]
133
+ @attributes['toc-placement'] = 'auto'
134
+ @attributes['stylesheet'] = ''
135
+ @attributes['linkcss'] = ''
135
136
 
136
137
  # language strings
137
138
  # TODO load these based on language settings
@@ -143,11 +144,26 @@ class Document < AbstractBlock
143
144
  @attributes['appendix-caption'] = 'Appendix'
144
145
  @attributes['example-caption'] = 'Example'
145
146
  @attributes['figure-caption'] = 'Figure'
147
+ #@attributes['listing-caption'] = 'Listing'
146
148
  @attributes['table-caption'] = 'Table'
147
149
  @attributes['toc-title'] = 'Table of Contents'
148
150
 
151
+ # attribute overrides are attributes that can only be set from the commandline
152
+ # a direct assignment effectively makes the attribute a constant
153
+ # assigning a nil value will result in the attribute being unset
149
154
  @attribute_overrides = options[:attributes] || {}
150
155
 
156
+ @attribute_overrides['asciidoctor'] = ''
157
+ @attribute_overrides['asciidoctor-version'] = VERSION
158
+
159
+ safe_mode_name = SafeMode.constants.detect {|l| SafeMode.const_get(l) == @safe}.to_s.downcase
160
+ @attribute_overrides['safe-mode-name'] = safe_mode_name
161
+ @attribute_overrides["safe-mode-#{safe_mode_name}"] = ''
162
+ @attribute_overrides['safe-mode-level'] = @safe
163
+
164
+ # sync the embedded attribute w/ the value of options...do not allow override
165
+ @attribute_overrides['embedded'] = @options[:header_footer] ? nil : ''
166
+
151
167
  # the only way to set the include-depth attribute is via the document options
152
168
  # 10 is the AsciiDoc default, though currently Asciidoctor only supports 1 level
153
169
  @attribute_overrides['include-depth'] ||= 10
@@ -159,8 +175,8 @@ class Document < AbstractBlock
159
175
  if @attribute_overrides['docdir']
160
176
  @base_dir = @attribute_overrides['docdir'] = File.expand_path(@attribute_overrides['docdir'])
161
177
  else
162
- # perhaps issue a warning here?
163
- @base_dir = @attribute_overrides['docdir'] = Dir.pwd
178
+ #puts 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested?
179
+ @base_dir = @attribute_overrides['docdir'] = File.expand_path(Dir.pwd)
164
180
  end
165
181
  else
166
182
  @base_dir = @attribute_overrides['docdir'] = File.expand_path(options[:base_dir])
@@ -176,7 +192,8 @@ class Document < AbstractBlock
176
192
  end
177
193
 
178
194
  if @safe >= SafeMode::SERVER
179
- # restrict document from setting source-highlighter and backend
195
+ # restrict document from setting linkcss, copycss, source-highlighter and backend
196
+ @attribute_overrides['copycss'] ||= nil
180
197
  @attribute_overrides['source-highlighter'] ||= nil
181
198
  @attribute_overrides['backend'] ||= DEFAULT_BACKEND
182
199
  # restrict document from seeing the docdir and trim docfile to relative path
@@ -184,17 +201,24 @@ class Document < AbstractBlock
184
201
  @attribute_overrides['docfile'] = @attribute_overrides['docfile'][(@attribute_overrides['docdir'].length + 1)..-1]
185
202
  end
186
203
  @attribute_overrides['docdir'] = ''
187
- # restrict document from enabling icons
188
204
  if @safe >= SafeMode::SECURE
205
+ # assign linkcss (preventing css embedding) unless disabled from the commandline
206
+ unless @attribute_overrides.fetch('linkcss', '').nil? || @attribute_overrides.has_key?('linkcss!')
207
+ @attribute_overrides['linkcss'] = ''
208
+ end
209
+ # restrict document from enabling icons
189
210
  @attribute_overrides['icons'] ||= nil
190
211
  end
191
212
  end
192
213
 
193
214
  @attribute_overrides.delete_if {|key, val|
194
215
  verdict = false
195
- # a nil or negative key undefines the attribute
196
- if val.nil? || key[-1..-1] == '!'
197
- @attributes.delete(key.chomp '!')
216
+ # a nil value undefines the attribute
217
+ if val.nil?
218
+ @attributes.delete(key)
219
+ # a negative key undefines the attribute
220
+ elsif key.end_with? '!'
221
+ @attributes.delete(key[0..-2])
198
222
  # otherwise it's an attribute assignment
199
223
  else
200
224
  # a value ending in @ indicates this attribute does not override
@@ -211,6 +235,12 @@ class Document < AbstractBlock
211
235
  @attributes['backend'] ||= DEFAULT_BACKEND
212
236
  @attributes['doctype'] ||= DEFAULT_DOCTYPE
213
237
  update_backend_attributes
238
+ # make toc and numbered the default for the docbook backend
239
+ # FIXME this doesn't take into account the backend being set in the document
240
+ #if @attributes.has_key?('basebackend-docbook')
241
+ # @attributes['toc'] = '' unless @attribute_overrides.has_key?('toc!')
242
+ # @attributes['numbered'] = '' unless @attribute_overrides.has_key?('numbered!')
243
+ #end
214
244
 
215
245
  if !@parent_document.nil?
216
246
  # don't need to do the extra processing within our own document
@@ -230,8 +260,10 @@ class Document < AbstractBlock
230
260
  @attributes['docdate'] ||= @attributes['localdate']
231
261
  @attributes['doctime'] ||= @attributes['localtime']
232
262
  @attributes['docdatetime'] ||= @attributes['localdatetime']
233
-
234
- @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', 'images'), 'icons')
263
+
264
+ # fallback directories
265
+ @attributes['stylesdir'] ||= '.'
266
+ @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons')
235
267
 
236
268
  # Now parse the lines in the reader into blocks
237
269
  Lexer.parse(@reader, self, :header_only => @options.fetch(:parse_header_only, false))
@@ -269,6 +301,18 @@ class Document < AbstractBlock
269
301
  (@attributes[name] = @counters[name])
270
302
  end
271
303
 
304
+ # Public: Increment the specified counter and store it in the block's attributes
305
+ #
306
+ # counter_name - the String name of the counter attribute
307
+ # block - the Block on which to save the counter
308
+ #
309
+ # returns the next number in the sequence for the specified counter
310
+ def counter_increment(counter_name, block)
311
+ val = counter(counter_name)
312
+ AttributeEntry.new(counter_name, val).save_to(block.attributes)
313
+ val
314
+ end
315
+
272
316
  # Internal: Get the next value in the sequence.
273
317
  #
274
318
  # Handles both integer and character sequences.
@@ -318,6 +362,11 @@ class Document < AbstractBlock
318
362
  !@parent_document.nil?
319
363
  end
320
364
 
365
+ def embedded?
366
+ # QUESTION should this be !@options[:header_footer] ?
367
+ @attributes.has_key? 'embedded'
368
+ end
369
+
321
370
  # Make the raw source for the Document available.
322
371
  def source
323
372
  @reader.source.join if @reader
@@ -396,6 +445,12 @@ class Document < AbstractBlock
396
445
  if @id.nil? && @attributes.has_key?('css-signature')
397
446
  @id = @attributes['css-signature']
398
447
  end
448
+
449
+ if @attributes.has_key? 'toc2'
450
+ @attributes['toc'] = ''
451
+ @attributes['toc-class'] ||= 'toc2'
452
+ end
453
+
399
454
  @original_attributes = @attributes.dup
400
455
  end
401
456
 
@@ -581,6 +636,53 @@ class Document < AbstractBlock
581
636
  @blocks.map {|b| b.render }.join
582
637
  end
583
638
 
639
+ # Public: Read the docinfo file(s) for inclusion in the
640
+ # document template
641
+ #
642
+ # If the docinfo1 attribute is set, read the docinfo.ext file. If the docinfo
643
+ # attribute is set, read the doc-name.docinfo.ext file. If the docinfo2
644
+ # attribute is set, read both files in that order.
645
+ #
646
+ # ext - The extension of the docinfo file(s). If not set, the extension
647
+ # will be determined based on the basebackend. (default: nil)
648
+ #
649
+ # returns The contents of the docinfo file(s)
650
+ def docinfo(ext = nil)
651
+ if safe >= SafeMode::SECURE
652
+ ''
653
+ else
654
+ if ext.nil?
655
+ case @attributes['basebackend']
656
+ when 'docbook'
657
+ ext = '.xml'
658
+ when 'html'
659
+ ext = '.html'
660
+ end
661
+ end
662
+
663
+ content = nil
664
+
665
+ docinfo = @attributes.has_key?('docinfo')
666
+ docinfo1 = @attributes.has_key?('docinfo1')
667
+ docinfo2 = @attributes.has_key?('docinfo2')
668
+ docinfo_filename = "docinfo#{ext}"
669
+ if docinfo1 || docinfo2
670
+ docinfo_path = normalize_system_path(docinfo_filename)
671
+ content = read_asset(docinfo_path)
672
+ end
673
+
674
+ if (docinfo || docinfo2) && @attributes.has_key?('docname')
675
+ docinfo_path = normalize_system_path("#{@attributes['docname']}-#{docinfo_filename}")
676
+ content2 = read_asset(docinfo_path)
677
+ unless content2.nil?
678
+ content = content.nil? ? content2 : "#{content}\n#{content2}"
679
+ end
680
+ end
681
+
682
+ content.nil? ? '' : content
683
+ end
684
+ end
685
+
584
686
  def to_s
585
687
  %[#{super.to_s} - #{doctitle}]
586
688
  end
@@ -21,6 +21,22 @@ module Helpers
21
21
  require name
22
22
  end
23
23
 
24
+ # Public: Encode a string for inclusion in a URI
25
+ #
26
+ # str - the string to encode
27
+ #
28
+ # returns an encoded version of the str
29
+ def self.encode_uri(str)
30
+ str.gsub(REGEXP[:uri_encode_chars]) do
31
+ match = $&
32
+ buf = ''
33
+ match.each_byte do |c|
34
+ buf << sprintf('%%%02X', c)
35
+ end
36
+ buf
37
+ end
38
+ end
39
+
24
40
  # Public: A generic capture output routine to be used in templates
25
41
  #def self.capture_output(*args, &block)
26
42
  # Proc.new { block.call(*args) }
@@ -23,7 +23,7 @@ module Asciidoctor
23
23
  # # => Asciidoctor::Block
24
24
  class Lexer
25
25
 
26
- BlockMatchData = Struct.new(:name, :tip, :terminator)
26
+ BlockMatchData = Struct.new(:context, :masq, :tip, :terminator)
27
27
 
28
28
  # Public: Make sure the Lexer object doesn't get initialized.
29
29
  #
@@ -77,7 +77,7 @@ class Lexer
77
77
  # check if the first line is the document title
78
78
  # if so, add a header to the document and parse the header metadata
79
79
  if is_next_line_document_title?(reader, block_attributes)
80
- document.id, document.title, _, _ = parse_section_title(reader)
80
+ document.id, document.title, _, _ = parse_section_title(reader, document)
81
81
  # QUESTION: should this be encapsulated in document?
82
82
  if document.id.nil? && block_attributes.has_key?('id')
83
83
  document.id = block_attributes.delete('id')
@@ -137,6 +137,8 @@ class Lexer
137
137
  def self.next_section(reader, parent, attributes = {})
138
138
  preamble = false
139
139
 
140
+ # FIXME if attributes[1] is a verbatim style, then don't check for section
141
+
140
142
  # check if we are at the start of processing the document
141
143
  # NOTE we could drop a hint in the attributes to indicate
142
144
  # that we are at a section title (so we don't have to check)
@@ -164,7 +166,13 @@ class Lexer
164
166
  # section title to next block of content
165
167
  attributes = attributes.delete_if {|k, v| k != 'title'}
166
168
  current_level = section.level
167
- expected_next_levels = [current_level + 1]
169
+ # subsections in preface & appendix in multipart books start at level 2
170
+ if current_level == 0 && section.special &&
171
+ section.document.doctype == 'book' && ['preface', 'appendix'].include?(section.sectname)
172
+ expected_next_levels = [current_level + 2]
173
+ else
174
+ expected_next_levels = [current_level + 1]
175
+ end
168
176
  end
169
177
 
170
178
  reader.skip_blank_lines
@@ -183,6 +191,7 @@ class Lexer
183
191
 
184
192
  next_level = is_next_line_section? reader, attributes
185
193
  if next_level
194
+ next_level += section.document.attr('leveloffset', 0).to_i
186
195
  doctype = parent.document.doctype
187
196
  if next_level == 0 && doctype != 'book'
188
197
  puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
@@ -215,7 +224,7 @@ class Lexer
215
224
  reader.skip_blank_lines
216
225
  end
217
226
 
218
- # prune the preamble if it has no content
227
+ # drop the preamble if it has no content
219
228
  if preamble && preamble.blocks.empty?
220
229
  section.delete_at(0)
221
230
  end
@@ -241,357 +250,365 @@ class Lexer
241
250
  # parent - The Document, Section or Block to which the next block belongs
242
251
  #
243
252
  # Returns a Section or Block object holding the parsed content of the processed lines
253
+ #--
254
+ # QUESTION should next_block have an option for whether it should keep looking until
255
+ # a block is found? right now it bails when it encounters a line to be skipped
244
256
  def self.next_block(reader, parent, attributes = {}, options = {})
245
257
  # Skip ahead to the block content
246
258
  skipped = reader.skip_blank_lines
247
259
 
248
- # bail if we've reached the end of the section content
260
+ # bail if we've reached the end of the parent block or document
249
261
  return nil unless reader.has_more_lines?
250
262
 
263
+ # check for option to find list item text only
264
+ # if skipped a line, assume a list continuation was
265
+ # used and block content is acceptable
251
266
  if options[:text] && skipped > 0
252
267
  options.delete(:text)
253
268
  end
254
-
255
- Debug.debug {
256
- msg = []
257
- msg << '/' * 64
258
- msg << 'next_block() - First two lines are:'
259
- msg.concat reader.peek_lines(2)
260
- msg << '/' * 64
261
- msg * "\n"
262
- }
263
269
 
264
- parse_metadata = options[:parse_metadata] || true
265
- parse_sections = options[:parse_sections] || false
270
+ parse_metadata = options.fetch(:parse_metadata, true)
271
+ #parse_sections = options.fetch(:parse_sections, false)
266
272
 
267
273
  document = parent.document
268
- context = parent.is_a?(Block) ? parent.context : nil
274
+ parent_context = parent.is_a?(Block) ? parent.context : nil
269
275
  block = nil
276
+ style = nil
277
+ explicit_style = nil
270
278
 
271
279
  while reader.has_more_lines? && block.nil?
280
+ # if parsing metadata, read until there is no more to read
272
281
  if parse_metadata && parse_block_metadata_line(reader, document, attributes, options)
273
282
  reader.advance
274
283
  next
275
- elsif parse_sections && context.nil? && is_next_line_section?(reader, attributes)
276
- block, attributes = next_section(reader, parent, attributes)
277
- break
284
+ #elsif parse_sections && parent_context.nil? && is_next_line_section?(reader, attributes)
285
+ # block, attributes = next_section(reader, parent, attributes)
286
+ # break
278
287
  end
279
288
 
289
+ # QUESTION introduce parsing context object?
280
290
  this_line = reader.get_line
281
-
291
+ delimited_block = false
282
292
  block_context = nil
283
293
  terminator = nil
294
+ # QUESTION put this inside call to rekey attributes?
295
+ if attributes.has_key? 1
296
+ explicit_style = attributes['style']
297
+ style = attributes['style'] = attributes[1]
298
+ end
299
+
284
300
  if delimited_blk_match = is_delimited_block?(this_line, true)
285
- block_context = delimited_blk_match.name
301
+ delimited_block = true
302
+ block_context = delimited_blk_match.context
286
303
  terminator = delimited_blk_match.terminator
304
+ if !style
305
+ style = attributes['style'] = block_context.to_s
306
+ elsif style != block_context.to_s
307
+ if delimited_blk_match.masq.include? style
308
+ block_context = style.to_sym
309
+ elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
310
+ block_context = :admonition
311
+ else
312
+ puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for #{block_context} block: #{style}"
313
+ style = block_context.to_s
314
+ end
315
+ end
287
316
  end
288
317
 
289
- # NOTE we're letting break lines (ruler, page_break, etc) have attributes
290
- if !options[:text] && block_context.nil? && (match = this_line.match(REGEXP[:break_line]))
291
- block = Block.new(parent, BREAK_LINES[match[0][0..2]])
292
- reader.skip_blank_lines
293
-
294
- elsif !options[:text] && block_context.nil? && (match = this_line.match(REGEXP[:image_blk]))
295
- block = Block.new(parent, :image)
296
- AttributeList.new(document.sub_attributes(match[2])).parse_into(attributes, ['alt', 'width', 'height'])
297
- target = block.sub_attributes(match[1])
298
- if !target.to_s.empty?
299
- attributes['target'] = target
300
- document.register(:images, target)
301
- attributes['alt'] ||= File.basename(target, File.extname(target))
302
- block.title = attributes['title']
303
- if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
304
- number = document.counter('figure-number')
305
- attributes['caption'] = "#{document.attributes['figure-caption']} #{number}. "
306
- Document::AttributeEntry.new('figure-number', number).save_to(attributes)
318
+ if !delimited_block
319
+
320
+ # this loop only executes once; used for flow control
321
+ # break once a block is found or at end of loop
322
+ # returns nil if the line must be dropped
323
+ # Implementation note - while(true) is twice as fast as loop
324
+ while true
325
+
326
+ # process lines verbatim
327
+ if !style.nil? && COMPLIANCE[:strict_verbatim_paragraphs] && VERBATIM_STYLES.include?(style)
328
+ block_context = style.to_sym
329
+ reader.unshift_line this_line
330
+ # advance to block parsing =>
331
+ break
307
332
  end
308
- else
309
- # drop the line if target resolves to nothing
310
- block = nil
311
- end
312
- reader.skip_blank_lines
313
333
 
314
- elsif block_context == :open
315
- # an open block is surrounded by '--' lines and has zero or more blocks inside
316
- buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
334
+ # process lines normally
335
+ if !options[:text]
336
+ # NOTE we're letting break lines (ruler, page_break, etc) have attributes
337
+ if (match = this_line.match(REGEXP[:break_line]))
338
+ block = Block.new(parent, BREAK_LINES[match[0][0..2]])
339
+ break
340
+
341
+ # TODO make this a media_blk and handle image, video & audio
342
+ elsif (match = this_line.match(REGEXP[:media_blk_macro]))
343
+ blk_ctx = match[1].to_sym
344
+ block = Block.new(parent, blk_ctx)
345
+ if blk_ctx == :image
346
+ posattrs = ['alt', 'width', 'height']
347
+ elsif blk_ctx == :video
348
+ posattrs = ['poster', 'width', 'height']
349
+ else
350
+ posattrs = []
351
+ end
317
352
 
318
- # Strip lines off end of block - not implemented yet
319
- # while buffer.has_more_lines? && buffer.last.strip.empty?
320
- # buffer.pop
321
- # end
353
+ unless style.nil? || explicit_style
354
+ attributes['alt'] = style if blk_ctx == :image
355
+ attributes.delete('style')
356
+ style = nil
357
+ end
322
358
 
323
- block = Block.new(parent, block_context)
324
- while buffer.has_more_lines?
325
- new_block = next_block(buffer, block)
326
- block.blocks << new_block unless new_block.nil?
327
- end
359
+ block.parse_attributes(match[3], posattrs,
360
+ :unescape_input => (blk_ctx == :image),
361
+ :sub_input => true,
362
+ :sub_result => false,
363
+ :into => attributes)
364
+ target = block.sub_attributes(match[2])
365
+ if target.empty?
366
+ # drop the line if target resolves to nothing
367
+ return nil
368
+ end
328
369
 
329
- # needs to come before list detection
330
- elsif block_context == :sidebar
331
- # sidebar is surrounded by '****' (4 or more '*' chars) lines
332
- # FIXME violates DRY because it's a duplication of quote parsing
333
- block = Block.new(parent, block_context)
334
- buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
370
+ attributes['target'] = target
371
+ block.title = attributes.delete('title') if attributes.has_key?('title')
372
+ if blk_ctx == :image
373
+ document.register(:images, target)
374
+ attributes['alt'] ||= File.basename(target, File.extname(target))
375
+ # QUESTION should video or audio have an auto-numbered caption?
376
+ block.assign_caption attributes.delete('caption'), 'figure'
377
+ end
378
+ break
335
379
 
336
- while buffer.has_more_lines?
337
- new_block = next_block(buffer, block)
338
- block.blocks << new_block unless new_block.nil?
339
- end
380
+ # NOTE we're letting the toc macro have attributes
381
+ elsif (match = this_line.match(REGEXP[:toc]))
382
+ block = Block.new(parent, :toc)
383
+ block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
384
+ break
340
385
 
341
- elsif block_context.nil? && (match = this_line.match(REGEXP[:colist]))
342
- block = Block.new(parent, :colist)
343
- attributes['style'] = 'arabic'
344
- items = []
345
- block.buffer = items
346
- reader.unshift_line this_line
347
- expected_index = 1
348
- begin
349
- # might want to move this check to a validate method
350
- if match[1].to_i != expected_index
351
- puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}"
386
+ end
352
387
  end
353
- list_item = next_list_item(reader, block, match)
354
- expected_index += 1
355
- if !list_item.nil?
356
- items << list_item
357
- coids = document.callouts.callout_ids(items.size)
358
- if !coids.empty?
359
- list_item.attributes['coids'] = coids
388
+
389
+ # haven't found anything yet, continue
390
+ if (match = this_line.match(REGEXP[:colist]))
391
+ block = Block.new(parent, :colist)
392
+ attributes['style'] = 'arabic'
393
+ items = []
394
+ block.buffer = items
395
+ reader.unshift_line this_line
396
+ expected_index = 1
397
+ begin
398
+ # might want to move this check to a validate method
399
+ if match[1].to_i != expected_index
400
+ puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}"
401
+ end
402
+ list_item = next_list_item(reader, block, match)
403
+ expected_index += 1
404
+ if !list_item.nil?
405
+ items << list_item
406
+ coids = document.callouts.callout_ids(items.size)
407
+ if !coids.empty?
408
+ list_item.attributes['coids'] = coids
409
+ else
410
+ puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}"
411
+ end
412
+ end
413
+ end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist])
414
+
415
+ document.callouts.next_list
416
+ break
417
+
418
+ elsif (match = this_line.match(REGEXP[:ulist]))
419
+ reader.unshift_line this_line
420
+ block = next_outline_list(reader, :ulist, parent)
421
+ break
422
+
423
+ elsif (match = this_line.match(REGEXP[:olist]))
424
+ reader.unshift_line this_line
425
+ block = next_outline_list(reader, :olist, parent)
426
+ # QUESTION move this logic to next_outline_list?
427
+ if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style')
428
+ marker = block.buffer.first.marker
429
+ if marker.start_with? '.'
430
+ # first one makes more sense, but second on is AsciiDoc-compliant
431
+ #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s
432
+ attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s
433
+ else
434
+ style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) }
435
+ attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s
436
+ end
437
+ end
438
+ break
439
+
440
+ elsif (match = this_line.match(REGEXP[:dlist]))
441
+ reader.unshift_line this_line
442
+ block = next_labeled_list(reader, match, parent)
443
+ break
444
+
445
+ elsif (style == 'float' || style == 'discrete') && is_section_title?(this_line, reader.peek_line)
446
+ reader.unshift_line this_line
447
+ float_id, float_title, float_level, _ = parse_section_title(reader, document)
448
+ float_id ||= attributes['id'] if attributes.has_key?('id')
449
+ block = Block.new(parent, :floating_title)
450
+ if float_id.nil? || float_id.empty?
451
+ # FIXME remove hack of creating throwaway Section to get at the generate_id method
452
+ tmp_sect = Section.new(parent)
453
+ tmp_sect.title = float_title
454
+ block.id = tmp_sect.generate_id
360
455
  else
361
- puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}"
456
+ block.id = float_id
362
457
  end
363
- end
364
- end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist])
365
-
366
- document.callouts.next_list
367
-
368
- elsif block_context.nil? && (match = this_line.match(REGEXP[:ulist]))
369
- AttributeList.rekey(attributes, ['style'])
370
- reader.unshift_line this_line
371
- block = next_outline_list(reader, :ulist, parent)
372
-
373
- elsif block_context.nil? && (match = this_line.match(REGEXP[:olist]))
374
- AttributeList.rekey(attributes, ['style'])
375
- reader.unshift_line this_line
376
- block = next_outline_list(reader, :olist, parent)
377
- # QUESTION move this logic to next_outline_list?
378
- if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style')
379
- marker = block.buffer.first.marker
380
- if marker.start_with? '.'
381
- # first one makes more sense, but second on is AsciiDoc-compliant
382
- #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s
383
- attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s
384
- else
385
- style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) }
386
- attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s
387
- end
388
- end
458
+ document.register(:ids, [block.id, float_title]) if block.id
459
+ block.level = float_level
460
+ block.title = float_title
461
+ break
389
462
 
390
- elsif block_context.nil? && (match = this_line.match(REGEXP[:dlist]))
391
- reader.unshift_line this_line
392
- block = next_labeled_list(reader, match, parent)
393
- AttributeList.rekey(attributes, ['style'])
394
-
395
- elsif block_context == :table
396
- # table is surrounded by lines starting with a | followed by 3 or more '=' chars
397
- AttributeList.rekey(attributes, ['style'])
398
- table_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
399
- block = next_table(table_reader, parent, attributes)
400
- block.title = attributes['title']
401
- if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
402
- number = document.counter('table-number')
403
- attributes['caption'] = "#{document.attributes['table-caption']} #{number}. "
404
- Document::AttributeEntry.new('table-number', number).save_to(attributes)
405
- end
406
-
407
- # FIXME violates DRY because it's a duplication of other block parsing
408
- elsif block_context == :example
409
- # example is surrounded by lines with 4 or more '=' chars
410
- AttributeList.rekey(attributes, ['style'])
411
- if admonition_style = ADMONITION_STYLES.detect {|s| attributes['style'] == s}
412
- block = Block.new(parent, :admonition)
413
- attributes['name'] = admonition_name = admonition_style.downcase
414
- attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
415
- else
416
- block = Block.new(parent, block_context)
417
- block.title = attributes['title']
418
- if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
419
- number = document.counter('example-number')
420
- attributes['caption'] = "#{document.attributes['example-caption']} #{number}. "
421
- Document::AttributeEntry.new('example-number', number).save_to(attributes)
463
+ # FIXME create another set for "passthrough" styles
464
+ # though partintro should likely be a dedicated block
465
+ elsif !style.nil? && style != 'normal' && style != 'partintro'
466
+ if PARAGRAPH_STYLES.include?(style)
467
+ block_context = style.to_sym
468
+ reader.unshift_line this_line
469
+ # advance to block parsing =>
470
+ break
471
+ elsif ADMONITION_STYLES.include?(style)
472
+ block_context = :admonition
473
+ reader.unshift_line this_line
474
+ # advance to block parsing =>
475
+ break
476
+ else
477
+ puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for paragraph: #{style}"
478
+ style = nil
479
+ # continue to process paragraph
480
+ end
422
481
  end
423
- end
424
- buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
425
482
 
426
- while buffer.has_more_lines?
427
- new_block = next_block(buffer, block)
428
- block.blocks << new_block unless new_block.nil?
429
- end
483
+ break_at_list = (skipped == 0 && parent_context.to_s.end_with?('list'))
430
484
 
431
- # FIXME violates DRY w/ non-delimited block listing
432
- elsif block_context == :listing || block_context == :fenced_code
433
- if block_context == :fenced_code
434
- attributes['style'] = 'source'
435
- lang = this_line[3..-1].strip
436
- attributes['language'] = lang unless lang.empty?
437
- terminator = terminator[0..2] if terminator.length > 3
438
- else
439
- AttributeList.rekey(attributes, ['style', 'language', 'linenums'])
440
- end
441
- buffer = reader.grab_lines_until(:terminator => terminator)
442
- buffer.last.chomp! unless buffer.empty?
443
- block = Block.new(parent, :listing, buffer)
444
- block.title = attributes['title']
445
- if document.attributes.has_key?('listing-caption') &&
446
- block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
447
- number = document.counter('listing-number')
448
- attributes['caption'] = "#{document.attributes['listing-caption']} #{number}. "
449
- Document::AttributeEntry.new('listing-number', number).save_to(attributes)
450
- end
485
+ # a literal paragraph is contiguous lines starting at least one space
486
+ if style != 'normal' && this_line.match(REGEXP[:lit_par])
487
+ # So we need to actually include this one in the grab_lines group
488
+ reader.unshift_line this_line
489
+ buffer = reader.grab_lines_until(
490
+ :break_on_blank_lines => true,
491
+ :break_on_list_continuation => true,
492
+ :preserve_last_line => true) {|line|
493
+ # a preceding blank line (skipped > 0) indicates we are in a list continuation
494
+ # and therefore we should not break at a list item
495
+ # (this won't stop breaking on item of same level since we've already parsed them out)
496
+ # QUESTION can we turn this block into a lambda or function call?
497
+ (break_at_list && line.match(REGEXP[:any_list])) ||
498
+ (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
499
+ }
451
500
 
452
- elsif block_context == :quote
453
- # multi-line verse or quote is surrounded by a block delimiter
454
- AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle'])
455
- quote_context = (attributes['style'] == 'verse' ? :verse : :quote)
456
- block_reader = Reader.new reader.grab_lines_until(:terminator => terminator)
501
+ # trim off the indentation equivalent to the size of the least indented line
502
+ if !buffer.empty?
503
+ offset = buffer.map {|line| line.match(REGEXP[:leading_blanks])[1].length }.min
504
+ if offset > 0
505
+ buffer = buffer.map {|l| l.sub(/^\s{1,#{offset}}/, '') }
506
+ end
507
+ end
457
508
 
458
- # only quote can have other section elements (as section block)
459
- section_body = (quote_context == :quote)
509
+ block = Block.new(parent, :literal, buffer)
510
+ # a literal gets special meaning inside of a definition list
511
+ if LIST_CONTEXTS.include?(parent_context)
512
+ attributes['options'] ||= []
513
+ # TODO this feels hacky, better way to distinguish from explicit literal block?
514
+ attributes['options'] << 'listparagraph'
515
+ end
460
516
 
461
- if section_body
462
- block = Block.new(parent, quote_context)
463
- while block_reader.has_more_lines?
464
- new_block = next_block(block_reader, block)
465
- block.blocks << new_block unless new_block.nil?
466
- end
467
- else
468
- block_reader.chomp_last!
469
- block = Block.new(parent, quote_context, block_reader.lines)
470
- end
517
+ # a paragraph is contiguous nonblank/noncontinuation lines
518
+ else
519
+ reader.unshift_line this_line
520
+ buffer = reader.grab_lines_until(
521
+ :break_on_blank_lines => true,
522
+ :break_on_list_continuation => true,
523
+ :preserve_last_line => true,
524
+ :skip_line_comments => true) {|line|
525
+ # a preceding blank line (skipped > 0) indicates we are in a list continuation
526
+ # and therefore we should not break at a list item
527
+ # (this won't stop breaking on item of same level since we've already parsed them out)
528
+ # QUESTION can we turn this block into a lambda or function call?
529
+ (break_at_list && line.match(REGEXP[:any_list])) ||
530
+ (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
531
+ }
471
532
 
472
- elsif block_context == :literal || block_context == :pass
473
- # literal is surrounded by '....' (4 or more '.' chars) lines
474
- # pass is surrounded by '++++' (4 or more '+' chars) lines
475
- buffer = reader.grab_lines_until(:terminator => terminator)
476
- buffer.last.chomp! unless buffer.empty?
477
- # a literal can masquerade as a listing
478
- if attributes[1] == 'listing'
479
- block_context = :listing
480
- end
481
- block = Block.new(parent, block_context, buffer)
482
-
483
- elsif this_line.match(REGEXP[:lit_par])
484
- # literal paragraph is contiguous lines starting with
485
- # one or more space or tab characters
486
-
487
- # So we need to actually include this one in the grab_lines group
488
- reader.unshift_line this_line
489
- buffer = reader.grab_lines_until(:preserve_last_line => true, :break_on_blank_lines => true) {|line|
490
- # labeled list terms can be indented, but a preceding blank indicates
491
- # we are in a list continuation and therefore literals should be strictly literal
492
- (context == :dlist && skipped == 0 && line.match(REGEXP[:dlist])) ||
493
- is_delimited_block?(line)
494
- }
533
+ # NOTE we need this logic because we've asked the reader to skip
534
+ # line comments, which may leave us w/ an empty buffer if those
535
+ # were the only lines found
536
+ if buffer.empty?
537
+ # call get_line since the reader preserved the last line
538
+ reader.get_line
539
+ return nil
540
+ end
541
+
542
+ catalog_inline_anchors(buffer.join, document)
495
543
 
496
- # trim off the indentation equivalent to the size of the least indented line
497
- if !buffer.empty?
498
- offset = buffer.map {|line| line.match(REGEXP[:leading_blanks])[1].length }.min
499
- if offset > 0
500
- buffer = buffer.map {|l| l.sub(/^\s{1,#{offset}}/, '') }
544
+ if !options[:text] && (admonition_match = buffer.first.match(REGEXP[:admonition_inline]))
545
+ buffer[0] = admonition_match.post_match.lstrip
546
+ block = Block.new(parent, :admonition, buffer)
547
+ attributes['style'] = admonition_match[1]
548
+ attributes['name'] = admonition_name = admonition_match[1].downcase
549
+ attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
550
+ else
551
+ # QUESTION is this necessary?
552
+ #if style == 'normal' && [' ', "\t"].include?(buffer.first[0..0])
553
+ # # QUESTION should we only trim leading blanks?
554
+ # buffer.map! &:lstrip
555
+ #end
556
+
557
+ block = Block.new(parent, :paragraph, buffer)
558
+ end
501
559
  end
502
- buffer.last.chomp!
503
- end
504
560
 
505
- block = Block.new(parent, :literal, buffer)
506
- # a literal gets special meaning inside of a definition list
507
- if LIST_CONTEXTS.include?(context)
508
- attributes['options'] ||= []
509
- # TODO this feels hacky, better way to distinguish from explicit literal block?
510
- attributes['options'] << 'listparagraph'
561
+ # forbid loop from executing more than once
562
+ break
511
563
  end
564
+ end
512
565
 
513
- ## these switches based on style need to come immediately before the else ##
566
+ # either delimited block or styled paragraph
567
+ if block.nil? && !block_context.nil?
568
+ case block_context
569
+ when :admonition
570
+ attributes['name'] = admonition_name = style.downcase
571
+ attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
572
+ block = build_block(block_context, :complex, terminator, parent, reader, attributes)
573
+
574
+ when :comment
575
+ reader.grab_lines_until(:break_on_blank_lines => true, :chomp_last_line => false)
576
+ return nil
577
+
578
+ when :example
579
+ block = build_block(block_context, :complex, terminator, parent, reader, attributes, true)
580
+
581
+ when :listing, :fenced_code, :source
582
+ if block_context == :fenced_code
583
+ style = attributes['style'] = 'source'
584
+ lang = this_line[3..-1].strip
585
+ attributes['language'] = lang unless lang.empty?
586
+ terminator = terminator[0..2] if terminator.length > 3
587
+ elsif block_context == :source
588
+ AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
589
+ end
590
+ block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, true)
514
591
 
515
- elsif attributes[1] == 'source' || attributes[1] == 'listing'
516
- if attributes[1] == 'source'
517
- AttributeList.rekey(attributes, ['style', 'language', 'linenums'])
518
- end
519
- reader.unshift_line this_line
520
- buffer = reader.grab_lines_until(:break_on_blank_lines => true)
521
- buffer.last.chomp! unless buffer.empty?
522
- block = Block.new(parent, :listing, buffer)
523
-
524
- elsif attributes[1] == 'literal'
525
- reader.unshift_line this_line
526
- buffer = reader.grab_lines_until(:break_on_blank_lines => true)
527
- buffer.last.chomp! unless buffer.empty?
528
- block = Block.new(parent, :literal, buffer)
529
-
530
- elsif admonition_style = ADMONITION_STYLES.detect{|s| attributes[1] == s}
531
- # an admonition preceded by [<TYPE>] and lasts until a blank line
532
- reader.unshift_line this_line
533
- buffer = reader.grab_lines_until(:break_on_blank_lines => true)
534
- buffer.last.chomp! unless buffer.empty?
535
- block = Block.new(parent, :admonition, buffer)
536
- attributes['style'] = admonition_style
537
- attributes['name'] = admonition_name = admonition_style.downcase
538
- attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
539
-
540
- elsif quote_context = [:quote, :verse].detect{|s| attributes[1] == s.to_s}
541
- # single-paragraph verse or quote is preceded by [verse] or [quote], respectively, and lasts until a blank line
542
- AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle'])
543
- reader.unshift_line this_line
544
- buffer = reader.grab_lines_until(:break_on_blank_lines => true)
545
- buffer.last.chomp! unless buffer.empty?
546
- block = Block.new(parent, quote_context, buffer)
547
-
548
- # a floating (i.e., discrete) title
549
- elsif ['float', 'discrete'].include?(attributes[1]) && is_section_title?(this_line, reader.peek_line)
550
- attributes['style'] = attributes[1]
551
- reader.unshift_line this_line
552
- float_id, float_title, float_level, _ = parse_section_title reader
553
- block = Block.new(parent, :floating_title)
554
- if float_id.nil? || float_id.empty?
555
- # FIXME remove hack of creating throwaway Section to get at the generate_id method
556
- tmp_sect = Section.new(parent)
557
- tmp_sect.title = float_title
558
- block.id = tmp_sect.generate_id
559
- else
560
- block.id = float_id
561
- @document.register(:ids, [float_id, float_title])
562
- end
563
- block.level = float_level
564
- block.title = float_title
592
+ when :literal
593
+ block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
594
+
595
+ when :pass
596
+ block = build_block(block_context, :simple, terminator, parent, reader, attributes)
565
597
 
566
- # a paragraph - contiguous nonblank/noncontinuation lines
567
- else
568
- reader.unshift_line this_line
569
- buffer = reader.grab_lines_until(:break_on_blank_lines => true, :preserve_last_line => true, :skip_line_comments => true) {|line|
570
- is_delimited_block?(line) || line.match(REGEXP[:attr_line]) ||
571
- # next list item can be directly adjacent to paragraph of previous list item
572
- context == :dlist && line.match(REGEXP[:dlist])
573
- # not sure if there are any cases when we need this check for other list types
574
- #LIST_CONTEXTS.include?(context) && line.match(REGEXP[context])
575
- }
598
+ when :open, :sidebar
599
+ block = build_block(block_context, :complex, terminator, parent, reader, attributes)
576
600
 
577
- # NOTE we need this logic because the reader is processing line
578
- # comments and that might leave us w/ an empty buffer
579
- if buffer.empty?
580
- reader.get_line
581
- break
582
- end
601
+ when :table
602
+ block_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
603
+ block = next_table(block_reader, parent, attributes)
583
604
 
584
- catalog_inline_anchors(buffer.join, document)
605
+ when :quote, :verse
606
+ AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
607
+ block = build_block(block_context, (block_context == :verse ? :verbatim : :complex), terminator, parent, reader, attributes)
585
608
 
586
- if !options[:text] && (admonition = buffer.first.match(Regexp.new('^(' + ADMONITION_STYLES.join('|') + '):\s+')))
587
- buffer[0] = admonition.post_match
588
- block = Block.new(parent, :admonition, buffer)
589
- attributes['style'] = admonition[1]
590
- attributes['name'] = admonition_name = admonition[1].downcase
591
- attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
592
609
  else
593
- buffer.last.chomp!
594
- block = Block.new(parent, :paragraph, buffer)
610
+ # this should only happen if there is a misconfiguration
611
+ raise "Unsupported block type #{block_context} at line #{reader.lineno}"
595
612
  end
596
613
  end
597
614
  end
@@ -599,8 +616,10 @@ class Lexer
599
616
  # when looking for nested content, one or more line comments, comment
600
617
  # blocks or trailing attribute lists could leave us without a block,
601
618
  # so handle accordingly
619
+ # REVIEW we may no longer need this check
602
620
  if !block.nil?
603
- block.id = attributes['id'] if attributes.has_key?('id')
621
+ # REVIEW seems like there is a better way to organize this wrap-up
622
+ block.id ||= attributes['id'] if attributes.has_key?('id')
604
623
  block.title = attributes['title'] unless block.title?
605
624
  block.caption ||= attributes['caption'] unless block.is_a?(Section)
606
625
  # AsciiDoc always use [id] as the reftext in HTML output,
@@ -643,9 +662,21 @@ class Lexer
643
662
  if DELIMITED_BLOCKS.has_key? tip
644
663
  # if tip is the full line
645
664
  if tl == line_len - 1
646
- return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true
665
+ #return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true
666
+ if return_match_data
667
+ context, masq = *DELIMITED_BLOCKS[tip]
668
+ BlockMatchData.new(context, masq, tip, tip)
669
+ else
670
+ true
671
+ end
647
672
  elsif match = line.match(REGEXP[:any_blk])
648
- return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true
673
+ #return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true
674
+ if return_match_data
675
+ context, masq = *DELIMITED_BLOCKS[tip]
676
+ BlockMatchData.new(context, masq, tip, match[0])
677
+ else
678
+ true
679
+ end
649
680
  else
650
681
  nil
651
682
  end
@@ -657,6 +688,48 @@ class Lexer
657
688
  end
658
689
  end
659
690
 
691
+ # whether a block supports complex content should be a config setting
692
+ # NOTE could invoke filter in here, before and after parsing
693
+ def self.build_block(block_context, content_type, terminator, parent, reader, attributes, supports_caption = false)
694
+ if terminator.nil?
695
+ if content_type == :verbatim
696
+ buffer = reader.grab_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true)
697
+ else
698
+ buffer = reader.grab_lines_until(
699
+ :break_on_blank_lines => true,
700
+ :break_on_list_continuation => true,
701
+ :preserve_last_line => true,
702
+ :skip_line_comments => true) {|line|
703
+ COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line]))
704
+ }
705
+ # QUESTION check for empty buffer?
706
+ end
707
+ elsif content_type != :complex
708
+ buffer = reader.grab_lines_until(:terminator => terminator, :chomp_last_line => true)
709
+ else
710
+ buffer = nil
711
+ block_reader = Reader.new reader.grab_lines_until(:terminator => terminator)
712
+ end
713
+
714
+ block = Block.new(parent, block_context, buffer)
715
+ # should supports_caption be necessary?
716
+ if supports_caption
717
+ block.title = attributes.delete('title') if attributes.has_key?('title')
718
+ block.assign_caption attributes.delete('caption')
719
+ end
720
+
721
+ if buffer.nil?
722
+ # we can look for blocks until there are no more lines (and not worry
723
+ # about sections) since the reader is confined within the boundaries of a
724
+ # delimited block
725
+ while block_reader.has_more_lines?
726
+ parsed_block = next_block(block_reader, block)
727
+ block.blocks << parsed_block unless parsed_block.nil?
728
+ end
729
+ end
730
+ block
731
+ end
732
+
660
733
  # Internal: Parse and construct an outline list Block from the current position of the Reader
661
734
  #
662
735
  # reader - The Reader from which to retrieve the outline list
@@ -744,9 +817,9 @@ class Lexer
744
817
  m = $~
745
818
  next if m[0].start_with? '\\'
746
819
  id, reftext = m[1].split(',')
747
- id.sub!(/^("|)(.*)\1$/, '\2')
820
+ id.sub!(REGEXP[:dbl_quoted], '\2')
748
821
  if !reftext.nil?
749
- reftext.sub!(/^("|)(.*)\1$/m, '\2')
822
+ reftext.sub!(REGEXP[:m_dbl_quoted], '\2')
750
823
  end
751
824
  document.register(:ids, [id, reftext])
752
825
  }
@@ -834,6 +907,9 @@ class Lexer
834
907
  # only relevant for :dlist
835
908
  options = {:text => !has_text}
836
909
 
910
+ # we can look for blocks until there are no more lines (and not worry
911
+ # about sections) since the reader is confined within the boundaries of a
912
+ # list
837
913
  while list_item_reader.has_more_lines?
838
914
  new_block = next_block(list_item_reader, list_block, {}, options)
839
915
  list_item.blocks << new_block unless new_block.nil?
@@ -898,7 +974,7 @@ class Lexer
898
974
  if continuation == :inactive
899
975
  continuation = :active
900
976
  has_text = true
901
- buffer[buffer.size - 1] = "\n" unless within_nested_list
977
+ buffer[-1] = "\n" unless within_nested_list
902
978
  end
903
979
 
904
980
  # dealing with adjacent list continuations (which is really a syntax error)
@@ -937,12 +1013,12 @@ class Lexer
937
1013
  if this_line.match(REGEXP[:lit_par])
938
1014
  reader.unshift_line this_line
939
1015
  buffer.concat reader.grab_lines_until(
940
- :preserve_last_line => true,
941
- :break_on_blank_lines => true,
942
- :break_on_list_continuation => true) {|line|
943
- # we may be in an indented list disguised as a literal paragraph
944
- # so we need to make sure we don't slurp up a legitimate sibling
945
- list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
1016
+ :preserve_last_line => true,
1017
+ :break_on_blank_lines => true,
1018
+ :break_on_list_continuation => true) {|line|
1019
+ # we may be in an indented list disguised as a literal paragraph
1020
+ # so we need to make sure we don't slurp up a legitimate sibling
1021
+ list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
946
1022
  }
947
1023
  continuation = :inactive
948
1024
  # let block metadata play out until we find the block
@@ -980,13 +1056,13 @@ class Lexer
980
1056
  if this_line.match(REGEXP[:lit_par])
981
1057
  reader.unshift_line this_line
982
1058
  buffer.concat reader.grab_lines_until(
983
- :preserve_last_line => true,
984
- :break_on_blank_lines => true,
985
- :break_on_list_continuation => true) {|line|
986
- # we may be in an indented list disguised as a literal paragraph
987
- # so we need to make sure we don't slurp up a legitimate sibling
988
- list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
989
- }
1059
+ :preserve_last_line => true,
1060
+ :break_on_blank_lines => true,
1061
+ :break_on_list_continuation => true) {|line|
1062
+ # we may be in an indented list disguised as a literal paragraph
1063
+ # so we need to make sure we don't slurp up a legitimate sibling
1064
+ list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
1065
+ }
990
1066
  # TODO any way to combine this with the check after skipping blank lines?
991
1067
  elsif is_sibling_list_item?(this_line, list_type, sibling_trait)
992
1068
  break
@@ -1053,7 +1129,7 @@ class Lexer
1053
1129
  # attributes - a Hash of attributes to assign to this section (default: {})
1054
1130
  def self.initialize_section(reader, parent, attributes = {})
1055
1131
  section = Section.new parent
1056
- section.id, section.title, section.level, _ = parse_section_title(reader)
1132
+ section.id, section.title, section.level, _ = parse_section_title(reader, section.document)
1057
1133
  if section.id.nil? && attributes.has_key?('id')
1058
1134
  section.id = attributes['id']
1059
1135
  else
@@ -1062,10 +1138,15 @@ class Lexer
1062
1138
  section.id ||= section.generate_id
1063
1139
  end
1064
1140
 
1141
+ if section.id
1142
+ section.document.register(:ids, [section.id, section.title])
1143
+ end
1144
+
1065
1145
  if attributes[1]
1066
1146
  section.sectname = attributes[1]
1067
1147
  section.special = true
1068
1148
  document = parent.document
1149
+ # FIXME refactor to use assign_caption (also check requirements)
1069
1150
  if section.sectname == 'appendix' &&
1070
1151
  !attributes.has_key?('caption') &&
1071
1152
  !document.attributes.has_key?('caption')
@@ -1087,14 +1168,7 @@ class Lexer
1087
1168
  #
1088
1169
  # line - the String line from under the section title.
1089
1170
  def self.section_level(line)
1090
- char = line.chomp.chars.to_a.uniq
1091
- case char
1092
- when ['=']; 0
1093
- when ['-']; 1
1094
- when ['~']; 2
1095
- when ['^']; 3
1096
- when ['+']; 4
1097
- end
1171
+ SECTION_LEVELS[line[0..0]]
1098
1172
  end
1099
1173
 
1100
1174
  #--
@@ -1168,13 +1242,14 @@ class Lexer
1168
1242
  # the Reader will be positioned at the line after the section title.
1169
1243
  #
1170
1244
  # reader - the source reader, positioned at a section title
1245
+ # document- the current document
1171
1246
  #
1172
1247
  # Examples
1173
1248
  #
1174
1249
  # reader.lines
1175
1250
  # # => ["Foo\n", "~~~\n"]
1176
1251
  #
1177
- # title, level, id, single = parse_section_title(reader)
1252
+ # title, level, id, single = parse_section_title(reader, document)
1178
1253
  #
1179
1254
  # title
1180
1255
  # # => "Foo"
@@ -1188,7 +1263,7 @@ class Lexer
1188
1263
  # line1
1189
1264
  # # => "==== Foo\n"
1190
1265
  #
1191
- # title, level, id, single = parse_section_title(reader)
1266
+ # title, level, id, single = parse_section_title(reader, document)
1192
1267
  #
1193
1268
  # title
1194
1269
  # # => "Foo"
@@ -1204,7 +1279,7 @@ class Lexer
1204
1279
  #
1205
1280
  #--
1206
1281
  # NOTE for efficiency, we don't reuse methods that check for a section title
1207
- def self.parse_section_title(reader)
1282
+ def self.parse_section_title(reader, document)
1208
1283
  line1 = reader.get_line
1209
1284
  sect_id = nil
1210
1285
  sect_title = nil
@@ -1232,7 +1307,10 @@ class Lexer
1232
1307
  reader.get_line
1233
1308
  end
1234
1309
  end
1235
- return [sect_id, sect_title, sect_level, single_line]
1310
+ if sect_level >= 0
1311
+ sect_level += document.attr('leveloffset', 0).to_i
1312
+ end
1313
+ [sect_id, sect_title, sect_level, single_line]
1236
1314
  end
1237
1315
 
1238
1316
  # Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
@@ -1255,55 +1333,102 @@ class Lexer
1255
1333
  metadata = {}
1256
1334
 
1257
1335
  if reader.has_more_lines? && !reader.peek_line.chomp.empty?
1336
+ author_metadata = {}
1337
+ keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
1258
1338
  author_line = reader.get_line
1259
- if match = author_line.match(REGEXP[:author_info])
1260
- metadata['firstname'] = fname = match[1].tr('_', ' ')
1261
- metadata['author'] = fname
1262
- metadata['authorinitials'] = fname[0, 1]
1263
- if !match[2].nil? && !match[3].nil?
1264
- metadata['middlename'] = mname = match[2].tr('_', ' ')
1265
- metadata['lastname'] = lname = match[3].tr('_', ' ')
1266
- metadata['author'] = [fname, mname, lname].join ' '
1267
- metadata['authorinitials'] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
1268
- elsif !match[2].nil?
1269
- metadata['lastname'] = lname = match[2].tr('_', ' ')
1270
- metadata['author'] = [fname, lname].join ' '
1271
- metadata['authorinitials'] = [fname[0, 1], lname[0, 1]].join
1339
+ author_line.split(REGEXP[:semicolon_delim]).each_with_index do |author_entry, idx|
1340
+ author_entry.strip!
1341
+ next if author_entry.empty?
1342
+ map = {}
1343
+ if idx.zero?
1344
+ keys.each do |key|
1345
+ map[key.to_sym] = key
1346
+ end
1347
+ else
1348
+ keys.each do |key|
1349
+ map[key.to_sym] = "#{key}_#{idx + 1}"
1350
+ end
1351
+ end
1352
+
1353
+ if match = author_entry.match(REGEXP[:author_info])
1354
+ author_metadata[map[:firstname]] = fname = match[1].tr('_', ' ')
1355
+ author_metadata[map[:author]] = fname
1356
+ author_metadata[map[:authorinitials]] = fname[0, 1]
1357
+ if !match[2].nil? && !match[3].nil?
1358
+ author_metadata[map[:middlename]] = mname = match[2].tr('_', ' ')
1359
+ author_metadata[map[:lastname]] = lname = match[3].tr('_', ' ')
1360
+ author_metadata[map[:author]] = [fname, mname, lname].join ' '
1361
+ author_metadata[map[:authorinitials]] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
1362
+ elsif !match[2].nil?
1363
+ author_metadata[map[:lastname]] = lname = match[2].tr('_', ' ')
1364
+ author_metadata[map[:author]] = [fname, lname].join ' '
1365
+ author_metadata[map[:authorinitials]] = [fname[0, 1], lname[0, 1]].join
1366
+ end
1367
+ author_metadata[map[:email]] = match[4] unless match[4].nil?
1368
+ else
1369
+ author_metadata[map[:author]] = author_metadata[map[:firstname]] = author_entry.strip.squeeze(' ')
1370
+ author_metadata[map[:authorinitials]] = author_metadata[map[:firstname]][0, 1]
1371
+ end
1372
+
1373
+ author_metadata['authorcount'] = idx + 1
1374
+ # only assign the _1 attributes if there are multiple authors
1375
+ if idx == 1
1376
+ keys.each do |key|
1377
+ author_metadata["#{key}_1"] = author_metadata[key] if author_metadata.has_key? key
1378
+ end
1379
+ end
1380
+ if idx.zero?
1381
+ author_metadata['authors'] = author_metadata[map[:author]]
1382
+ else
1383
+ author_metadata['authors'] = "#{author_metadata['authors']}, #{author_metadata[map[:author]]}"
1272
1384
  end
1273
- metadata['email'] = match[4] unless match[4].nil?
1274
- else
1275
- metadata['author'] = metadata['firstname'] = author_line.strip.squeeze(' ')
1276
- metadata['authorinitials'] = metadata['firstname'][0, 1]
1277
1385
  end
1278
1386
 
1279
- # NOTE this will discard away any comment lines, but not skip blank lines
1387
+ # apply header subs and assign to document
1388
+ if !document.nil?
1389
+ author_metadata.map do |key, val|
1390
+ val = val.is_a?(String) ? document.apply_header_subs(val) : val
1391
+ document.attributes[key] = val if !document.attributes.has_key?(key)
1392
+ val
1393
+ end
1394
+ end
1395
+
1396
+ metadata = author_metadata.dup
1397
+
1398
+ # NOTE this will discard any comment lines, but not skip blank lines
1280
1399
  process_attribute_entries(reader, document)
1281
1400
 
1401
+ rev_metadata = {}
1402
+
1282
1403
  if reader.has_more_lines? && !reader.peek_line.chomp.empty?
1283
1404
  rev_line = reader.get_line
1284
1405
  if match = rev_line.match(REGEXP[:revision_info])
1285
- metadata['revdate'] = match[2].strip
1286
- metadata['revnumber'] = match[1].rstrip unless match[1].nil?
1287
- metadata['revremark'] = match[3].rstrip unless match[3].nil?
1406
+ rev_metadata['revdate'] = match[2].strip
1407
+ rev_metadata['revnumber'] = match[1].rstrip unless match[1].nil?
1408
+ rev_metadata['revremark'] = match[3].rstrip unless match[3].nil?
1288
1409
  else
1289
1410
  # throw it back
1290
1411
  reader.unshift_line rev_line
1291
1412
  end
1292
1413
  end
1293
1414
 
1294
- # NOTE this will discard away any comment lines, but not skip blank lines
1295
- process_attribute_entries(reader, document)
1296
-
1297
- reader.skip_blank_lines
1298
-
1299
1415
  # apply header subs and assign to document
1300
1416
  if !document.nil?
1301
- metadata.map do |key, val|
1417
+ rev_metadata.map do |key, val|
1302
1418
  val = document.apply_header_subs(val)
1303
1419
  document.attributes[key] = val if !document.attributes.has_key?(key)
1304
1420
  val
1305
1421
  end
1306
1422
  end
1423
+
1424
+ rev_metadata.each {|k, v|
1425
+ metadata[k] = v
1426
+ }
1427
+
1428
+ # NOTE this will discard any comment lines, but not skip blank lines
1429
+ process_attribute_entries(reader, document)
1430
+
1431
+ reader.skip_blank_lines
1307
1432
  end
1308
1433
 
1309
1434
  metadata
@@ -1372,7 +1497,7 @@ class Lexer
1372
1497
  parent.document.register(:ids, [id, reftext])
1373
1498
  end
1374
1499
  elsif match = next_line.match(REGEXP[:blk_attr_list])
1375
- AttributeList.new(parent.document.sub_attributes(match[1]), parent.document).parse_into(attributes)
1500
+ parent.document.parse_attributes(match[1], [], :sub_input => true, :into => attributes)
1376
1501
  # NOTE title doesn't apply to section, but we need to stash it for the first block
1377
1502
  # TODO should issue an error if this is found above the document title
1378
1503
  elsif !options[:text] && (match = next_line.match(REGEXP[:blk_title]))
@@ -1566,6 +1691,8 @@ class Lexer
1566
1691
  # returns an instance of Asciidoctor::Table parsed from the provided reader
1567
1692
  def self.next_table(table_reader, parent, attributes)
1568
1693
  table = Table.new(parent, attributes)
1694
+ table.title = attributes.delete('title') if attributes.has_key?('title')
1695
+ table.assign_caption attributes.delete('caption')
1569
1696
 
1570
1697
  if attributes.has_key? 'cols'
1571
1698
  table.create_columns(parse_col_specs(attributes['cols']))