asciidoctor 2.0.13 → 2.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +151 -30
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +9 -12
  5. data/README-fr.adoc +9 -12
  6. data/README-jp.adoc +10 -13
  7. data/README-zh_CN.adoc +9 -12
  8. data/README.adoc +40 -19
  9. data/asciidoctor.gemspec +2 -9
  10. data/data/locale/attributes-fr.adoc +2 -2
  11. data/data/locale/attributes-th.adoc +23 -0
  12. data/data/locale/attributes-vi.adoc +23 -0
  13. data/data/stylesheets/asciidoctor-default.css +54 -53
  14. data/data/stylesheets/coderay-asciidoctor.css +9 -9
  15. data/lib/asciidoctor/abstract_block.rb +11 -9
  16. data/lib/asciidoctor/abstract_node.rb +9 -8
  17. data/lib/asciidoctor/attribute_list.rb +1 -1
  18. data/lib/asciidoctor/block.rb +6 -6
  19. data/lib/asciidoctor/cli/invoker.rb +1 -2
  20. data/lib/asciidoctor/cli/options.rb +25 -25
  21. data/lib/asciidoctor/convert.rb +1 -0
  22. data/lib/asciidoctor/converter/docbook5.rb +45 -26
  23. data/lib/asciidoctor/converter/html5.rb +130 -102
  24. data/lib/asciidoctor/converter/manpage.rb +69 -64
  25. data/lib/asciidoctor/converter/template.rb +12 -13
  26. data/lib/asciidoctor/converter.rb +6 -4
  27. data/lib/asciidoctor/core_ext/hash/merge.rb +1 -1
  28. data/lib/asciidoctor/document.rb +61 -57
  29. data/lib/asciidoctor/extensions.rb +20 -12
  30. data/lib/asciidoctor/list.rb +2 -6
  31. data/lib/asciidoctor/load.rb +11 -9
  32. data/lib/asciidoctor/logging.rb +10 -8
  33. data/lib/asciidoctor/parser.rb +177 -193
  34. data/lib/asciidoctor/path_resolver.rb +3 -3
  35. data/lib/asciidoctor/reader.rb +73 -72
  36. data/lib/asciidoctor/rx.rb +5 -4
  37. data/lib/asciidoctor/section.rb +7 -0
  38. data/lib/asciidoctor/substitutors.rb +121 -121
  39. data/lib/asciidoctor/syntax_highlighter/coderay.rb +2 -1
  40. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +1 -1
  41. data/lib/asciidoctor/syntax_highlighter/pygments.rb +16 -7
  42. data/lib/asciidoctor/syntax_highlighter/rouge.rb +2 -1
  43. data/lib/asciidoctor/syntax_highlighter.rb +8 -11
  44. data/lib/asciidoctor/table.rb +18 -20
  45. data/lib/asciidoctor/timings.rb +3 -3
  46. data/lib/asciidoctor/version.rb +1 -1
  47. data/lib/asciidoctor.rb +10 -10
  48. data/man/asciidoctor.1 +26 -28
  49. data/man/asciidoctor.adoc +33 -27
  50. metadata +8 -62
@@ -89,10 +89,10 @@ class Parser
89
89
  #
90
90
  # returns the Document object
91
91
  def self.parse(reader, document, options = {})
92
- block_attributes = parse_document_header(reader, document)
92
+ block_attributes = parse_document_header(reader, document, (header_only = options[:header_only]))
93
93
 
94
94
  # NOTE don't use a postfix conditional here as it's known to confuse JRuby in certain circumstances
95
- unless options[:header_only]
95
+ unless header_only
96
96
  while reader.has_more_lines?
97
97
  new_section, block_attributes = next_section(reader, document, block_attributes)
98
98
  if new_section
@@ -117,7 +117,7 @@ class Parser
117
117
  # which are automatically removed by the reader.
118
118
  #
119
119
  # returns the Hash of orphan block attributes captured above the header
120
- def self.parse_document_header(reader, document)
120
+ def self.parse_document_header(reader, document, header_only = false)
121
121
  # capture lines of block-level metadata and plow away comment lines that precede first block
122
122
  block_attrs = reader.skip_blank_lines ? (parse_block_metadata_lines reader, document) : {}
123
123
  doc_attrs = document.attributes
@@ -125,6 +125,7 @@ class Parser
125
125
  # special case, block title is not allowed above document title,
126
126
  # carry attributes over to the document body
127
127
  if (implicit_doctitle = is_next_line_doctitle? reader, block_attrs, doc_attrs['leveloffset']) && block_attrs['title']
128
+ doc_attrs['authorcount'] = 0
128
129
  return document.finalize_header block_attrs, false
129
130
  end
130
131
 
@@ -144,7 +145,10 @@ class Parser
144
145
  l0_section_title = nil
145
146
  else
146
147
  document.title = l0_section_title
147
- doc_attrs['doctitle'] = doctitle_attr_val = document.apply_header_subs l0_section_title
148
+ if (doc_attrs['doctitle'] = doctitle_attr_val = document.sub_specialchars l0_section_title).include? ATTR_REF_HEAD
149
+ # QUESTION should we defer substituting attributes until the end of the header? or should we substitute again if necessary?
150
+ doc_attrs['doctitle'] = doctitle_attr_val = document.sub_attributes doctitle_attr_val, attribute_missing: 'skip'
151
+ end
148
152
  end
149
153
  document.header.source_location = source_location if source_location
150
154
  # default to compat-mode if document has setext doctitle
@@ -165,7 +169,7 @@ class Parser
165
169
  end
166
170
  block_attrs.clear
167
171
  (modified_attrs = document.instance_variable_get :@attributes_modified).delete 'doctitle'
168
- parse_header_metadata reader, document
172
+ parse_header_metadata reader, document, nil
169
173
  if modified_attrs.include? 'doctitle'
170
174
  if (val = doc_attrs['doctitle']).nil_or_empty? || val == doctitle_attr_val
171
175
  doc_attrs['doctitle'] = doctitle_attr_val
@@ -176,10 +180,19 @@ class Parser
176
180
  modified_attrs << 'doctitle'
177
181
  end
178
182
  document.register :refs, [doc_id, document] if doc_id
183
+ elsif (author = doc_attrs['author'])
184
+ author_metadata = process_authors author, true, false
185
+ author_metadata.delete 'authorinitials' if doc_attrs['authorinitials']
186
+ doc_attrs.update author_metadata
187
+ elsif (author = doc_attrs['authors'])
188
+ author_metadata = process_authors author, true
189
+ doc_attrs.update author_metadata
190
+ else
191
+ doc_attrs['authorcount'] = 0
179
192
  end
180
193
 
181
194
  # parse title and consume name section of manpage document
182
- parse_manpage_header reader, document, block_attrs if document.doctype == 'manpage'
195
+ parse_manpage_header reader, document, block_attrs, header_only if document.doctype == 'manpage'
183
196
 
184
197
  # NOTE block_attrs are the block-level attributes (not document attributes) that
185
198
  # precede the first line of content (document title, first section or first block)
@@ -189,7 +202,7 @@ class Parser
189
202
  # Public: Parses the manpage header of the AsciiDoc source read from the Reader
190
203
  #
191
204
  # returns Nothing
192
- def self.parse_manpage_header(reader, document, block_attributes)
205
+ def self.parse_manpage_header(reader, document, block_attributes, header_only = false)
193
206
  if ManpageTitleVolnumRx =~ (doc_attrs = document.attributes)['doctitle']
194
207
  doc_attrs['manvolnum'] = manvolnum = $2
195
208
  doc_attrs['mantitle'] = (((mantitle = $1).include? ATTR_REF_HEAD) ? (document.sub_attributes mantitle) : mantitle).downcase
@@ -206,6 +219,8 @@ class Parser
206
219
  doc_attrs['docname'] = manname
207
220
  doc_attrs['outfilesuffix'] = %(.#{manvolnum})
208
221
  end
222
+ elsif header_only
223
+ # done
209
224
  else
210
225
  reader.skip_blank_lines
211
226
  reader.save
@@ -418,9 +433,6 @@ class Parser
418
433
 
419
434
  (intro || section).blocks << new_block
420
435
  attributes.clear
421
- #else
422
- # # don't clear attributes if we don't find a block because they may
423
- # # be trailing attributes that didn't get associated with a block
424
436
  end
425
437
  end
426
438
 
@@ -452,10 +464,10 @@ class Parser
452
464
  end
453
465
 
454
466
  # The attributes returned here are orphaned attributes that fall at the end
455
- # of a section that need to get transfered to the next section
467
+ # of a section that need to get transferred to the next section
456
468
  # see "trailing block attributes transfer to the following section" in
457
469
  # test/attributes_test.rb for an example
458
- [section != parent ? section : nil, attributes.merge]
470
+ [section == parent ? nil : section, attributes.merge]
459
471
  end
460
472
 
461
473
  # Public: Parse and return the next Block at the Reader's current location
@@ -647,7 +659,7 @@ class Parser
647
659
  if (default_attrs = ext_config[:default_attrs])
648
660
  attributes.update(default_attrs) {|_, old_v| old_v }
649
661
  end
650
- if (block = extension.process_method[parent, target, attributes])
662
+ if (block = extension.process_method[parent, target, attributes]) && block != parent
651
663
  attributes.replace block.attributes
652
664
  break
653
665
  else
@@ -823,8 +835,8 @@ class Parser
823
835
  if comma_idx > 0
824
836
  language = (language.slice 0, comma_idx).strip
825
837
  attributes['linenums'] = '' if comma_idx < ll - 4
826
- else
827
- attributes['linenums'] = '' if ll > 4
838
+ elsif ll > 4
839
+ attributes['linenums'] = ''
828
840
  end
829
841
  else
830
842
  language = language.lstrip
@@ -905,9 +917,7 @@ class Parser
905
917
  # FIXME title and caption should be assigned when block is constructed (though we need to handle all cases)
906
918
  if attributes['title']
907
919
  block.title = block_title = attributes.delete 'title'
908
- if (caption_attr_name = CAPTION_ATTRIBUTE_NAMES[block.context]) && document.attributes[caption_attr_name]
909
- block.assign_caption (attributes.delete 'caption')
910
- end
920
+ block.assign_caption attributes.delete 'caption' if CAPTION_ATTRIBUTE_NAMES[block.context]
911
921
  end
912
922
  # TODO eventually remove the style attribute from the attributes hash
913
923
  #block.style = attributes.delete 'style'
@@ -969,17 +979,12 @@ class Parser
969
979
  # special case for fenced code blocks
970
980
  if Compliance.markdown_syntax && (tip.start_with? '`')
971
981
  if tip_len == 4
972
- if tip == '````'
973
- return
974
- elsif (tip = tip.chop) == '```'
975
- line = tip
976
- line_len = tip_len = 3
977
- else
982
+ if tip == '````' || (tip = tip.chop) != '```'
978
983
  return
979
984
  end
980
- elsif tip == '```'
981
- # keep it
982
- else
985
+ line = tip
986
+ line_len = tip_len = 3
987
+ elsif tip != '```'
983
988
  return
984
989
  end
985
990
  elsif tip_len == 3
@@ -997,9 +1002,10 @@ class Parser
997
1002
  # if terminator is false, that means the all the lines in the reader should be parsed
998
1003
  # NOTE could invoke filter in here, before and after parsing
999
1004
  def self.build_block(block_context, content_model, terminator, parent, reader, attributes, options = {})
1000
- if content_model == :skip
1005
+ case content_model
1006
+ when :skip
1001
1007
  skip_processing, parse_as_content_model = true, :simple
1002
- elsif content_model == :raw
1008
+ when :raw
1003
1009
  skip_processing, parse_as_content_model = false, :simple
1004
1010
  else
1005
1011
  skip_processing, parse_as_content_model = false, content_model
@@ -1028,14 +1034,15 @@ class Parser
1028
1034
  block_reader = Reader.new reader.read_lines_until(terminator: terminator, skip_processing: skip_processing, context: block_context, cursor: :at_mark), block_cursor
1029
1035
  end
1030
1036
 
1031
- if content_model == :verbatim
1037
+ case content_model
1038
+ when :verbatim
1032
1039
  tab_size = (attributes['tabsize'] || parent.document.attributes['tabsize']).to_i
1033
1040
  if (indent = attributes['indent'])
1034
1041
  adjust_indentation! lines, indent.to_i, tab_size
1035
1042
  elsif tab_size > 0
1036
1043
  adjust_indentation! lines, -1, tab_size
1037
1044
  end
1038
- elsif content_model == :skip
1045
+ when :skip
1039
1046
  # QUESTION should we still invoke process method if extension is specified?
1040
1047
  return
1041
1048
  end
@@ -1043,12 +1050,12 @@ class Parser
1043
1050
  if (extension = options[:extension])
1044
1051
  # QUESTION do we want to delete the style?
1045
1052
  attributes.delete('style')
1046
- if (block = extension.process_method[parent, block_reader || (Reader.new lines), attributes.merge])
1053
+ if (block = extension.process_method[parent, block_reader || (Reader.new lines), attributes.merge]) && block != parent
1047
1054
  attributes.replace block.attributes
1048
1055
  # FIXME if the content model is set to compound, but we only have simple in this context, then
1049
1056
  # forcefully set the content_model to simple to prevent parsing blocks from children
1050
1057
  # TODO document this behavior!!
1051
- if block.content_model == :compound && !(lines = block.lines).empty?
1058
+ if block.content_model == :compound && Block === block && !(lines = block.lines).empty?
1052
1059
  content_model = :compound
1053
1060
  block_reader = Reader.new lines
1054
1061
  end
@@ -1296,7 +1303,8 @@ class Parser
1296
1303
  has_text = true
1297
1304
  list_item = ListItem.new(list_block, (item_text = match[2]))
1298
1305
  list_item.source_location = reader.cursor if list_block.document.sourcemap
1299
- if list_type == :ulist
1306
+ case list_type
1307
+ when :ulist
1300
1308
  list_item.marker = sibling_trait
1301
1309
  if item_text.start_with?('[')
1302
1310
  if style && style == 'bibliography'
@@ -1314,13 +1322,13 @@ class Parser
1314
1322
  list_item.text = item_text.slice(4, item_text.length)
1315
1323
  end
1316
1324
  end
1317
- elsif list_type == :olist
1325
+ when :olist
1318
1326
  sibling_trait, implicit_style = resolve_ordered_list_marker(sibling_trait, (ordinal = list_block.items.size), true, reader)
1319
1327
  list_item.marker = sibling_trait
1320
1328
  if ordinal == 0 && !style
1321
1329
  # using list level makes more sense, but we don't track it
1322
- # basing style on marker level is compliant with AsciiDoc Python
1323
- list_block.style = implicit_style || ((ORDERED_LIST_STYLES[sibling_trait.length - 1] || 'arabic').to_s)
1330
+ # basing style on marker level is compliant with AsciiDoc.py
1331
+ list_block.style = implicit_style || (ORDERED_LIST_STYLES[sibling_trait.length - 1] || 'arabic').to_s
1324
1332
  end
1325
1333
  if item_text.start_with?('[[') && LeadingInlineAnchorRx =~ item_text
1326
1334
  catalog_inline_anchor $1, $2, list_item, reader
@@ -1446,95 +1454,89 @@ class Parser
1446
1454
  # FIXME to be AsciiDoc compliant, we shouldn't break if style in attribute line is "literal" (i.e., [literal])
1447
1455
  elsif dlist && continuation != :active && (BlockAttributeLineRx.match? this_line)
1448
1456
  break
1449
- else
1450
- if continuation == :active && !this_line.empty?
1451
- # literal paragraphs have special considerations (and this is one of
1452
- # two entry points into one)
1453
- # if we don't process it as a whole, then a line in it that looks like a
1454
- # list item will throw off the exit from it
1455
- if LiteralParagraphRx.match? this_line
1456
- reader.unshift_line this_line
1457
- if dlist
1458
- # we may be in an indented list disguised as a literal paragraph
1459
- # so we need to make sure we don't slurp up a legitimate sibling
1460
- buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true) {|line| is_sibling_list_item? line, list_type, sibling_trait }
1461
- else
1462
- buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
1463
- end
1464
- continuation = :inactive
1465
- # let block metadata play out until we find the block
1466
- elsif (BlockTitleRx.match? this_line) || (BlockAttributeLineRx.match? this_line) || (AttributeEntryRx.match? this_line)
1467
- buffer << this_line
1457
+ elsif continuation == :active && !this_line.empty?
1458
+ # literal paragraphs have special considerations (and this is one of
1459
+ # two entry points into one)
1460
+ # if we don't process it as a whole, then a line in it that looks like a
1461
+ # list item will throw off the exit from it
1462
+ if LiteralParagraphRx.match? this_line
1463
+ reader.unshift_line this_line
1464
+ if dlist
1465
+ # we may be in an indented list disguised as a literal paragraph
1466
+ # so we need to make sure we don't slurp up a legitimate sibling
1467
+ buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true) {|line| is_sibling_list_item? line, list_type, sibling_trait }
1468
1468
  else
1469
- if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx].match? this_line }
1470
- within_nested_list = true
1471
- if nested_list_type == :dlist && $3.nil_or_empty?
1472
- # get greedy again
1473
- has_text = false
1474
- end
1475
- end
1476
- buffer << this_line
1477
- continuation = :inactive
1469
+ buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
1478
1470
  end
1479
- elsif prev_line && prev_line.empty?
1480
- # advance to the next line of content
1481
- if this_line.empty?
1482
- # stop reading if we reach eof
1483
- break unless (this_line = reader.skip_blank_lines && reader.read_line)
1484
- # stop reading if we hit a sibling list item
1485
- break if is_sibling_list_item? this_line, list_type, sibling_trait
1471
+ continuation = :inactive
1472
+ # let block metadata play out until we find the block
1473
+ elsif (BlockTitleRx.match? this_line) || (BlockAttributeLineRx.match? this_line) || (AttributeEntryRx.match? this_line)
1474
+ buffer << this_line
1475
+ else
1476
+ if (nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx].match? this_line })
1477
+ within_nested_list = true
1478
+ if nested_list_type == :dlist && $3.nil_or_empty?
1479
+ # get greedy again
1480
+ has_text = false
1481
+ end
1486
1482
  end
1483
+ buffer << this_line
1484
+ continuation = :inactive
1485
+ end
1486
+ elsif prev_line && prev_line.empty?
1487
+ # advance to the next line of content
1488
+ if this_line.empty?
1489
+ # stop reading if we reach eof
1490
+ break unless (this_line = reader.skip_blank_lines && reader.read_line)
1491
+ # stop reading if we hit a sibling list item
1492
+ break if is_sibling_list_item? this_line, list_type, sibling_trait
1493
+ end
1487
1494
 
1488
- if this_line == LIST_CONTINUATION
1489
- detached_continuation = buffer.size
1495
+ if this_line == LIST_CONTINUATION
1496
+ detached_continuation = buffer.size
1497
+ buffer << this_line
1498
+ elsif has_text # has_text only relevant for dlist, which is more greedy until it has text for an item; has_text is always true for all other lists
1499
+ # in this block, we have to see whether we stay in the list
1500
+ # TODO any way to combine this with the check after skipping blank lines?
1501
+ if is_sibling_list_item?(this_line, list_type, sibling_trait)
1502
+ break
1503
+ elsif (nested_list_type = NESTABLE_LIST_CONTEXTS.find {|ctx| ListRxMap[ctx] =~ this_line })
1490
1504
  buffer << this_line
1491
- else
1492
- # has_text is only relevant for dlist, which is more greedy until it has text for an item
1493
- # for all other lists, has_text is always true
1494
- # in this block, we have to see whether we stay in the list
1495
- if has_text
1496
- # TODO any way to combine this with the check after skipping blank lines?
1497
- if is_sibling_list_item?(this_line, list_type, sibling_trait)
1498
- break
1499
- elsif nested_list_type = NESTABLE_LIST_CONTEXTS.find {|ctx| ListRxMap[ctx] =~ this_line }
1500
- buffer << this_line
1501
- within_nested_list = true
1502
- if nested_list_type == :dlist && $3.nil_or_empty?
1503
- # get greedy again
1504
- has_text = false
1505
- end
1506
- # slurp up any literal paragraph offset by blank lines
1507
- # NOTE we have to check for indented list items first
1508
- elsif LiteralParagraphRx.match? this_line
1509
- reader.unshift_line this_line
1510
- if dlist
1511
- # we may be in an indented list disguised as a literal paragraph
1512
- # so we need to make sure we don't slurp up a legitimate sibling
1513
- buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true) {|line| is_sibling_list_item? line, list_type, sibling_trait }
1514
- else
1515
- buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
1516
- end
1517
- else
1518
- break
1519
- end
1520
- else # only dlist in need of item text, so slurp it up!
1521
- # pop the blank line so it's not interpretted as a list continuation
1522
- buffer.pop unless within_nested_list
1523
- buffer << this_line
1524
- has_text = true
1525
- end
1526
- end
1527
- else
1528
- has_text = true unless this_line.empty?
1529
- if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx] =~ this_line }
1530
1505
  within_nested_list = true
1531
1506
  if nested_list_type == :dlist && $3.nil_or_empty?
1532
1507
  # get greedy again
1533
1508
  has_text = false
1534
1509
  end
1510
+ # slurp up any literal paragraph offset by blank lines
1511
+ # NOTE we have to check for indented list items first
1512
+ elsif LiteralParagraphRx.match? this_line
1513
+ reader.unshift_line this_line
1514
+ if dlist
1515
+ # we may be in an indented list disguised as a literal paragraph
1516
+ # so we need to make sure we don't slurp up a legitimate sibling
1517
+ buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true) {|line| is_sibling_list_item? line, list_type, sibling_trait }
1518
+ else
1519
+ buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
1520
+ end
1521
+ else
1522
+ break
1535
1523
  end
1524
+ else # only dlist in need of item text, so slurp it up!
1525
+ # pop the blank line so it's not interpreted as a list continuation
1526
+ buffer.pop unless within_nested_list
1536
1527
  buffer << this_line
1528
+ has_text = true
1529
+ end
1530
+ else
1531
+ has_text = true unless this_line.empty?
1532
+ if (nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx] =~ this_line })
1533
+ within_nested_list = true
1534
+ if nested_list_type == :dlist && $3.nil_or_empty?
1535
+ # get greedy again
1536
+ has_text = false
1537
+ end
1537
1538
  end
1539
+ buffer << this_line
1538
1540
  end
1539
1541
  this_line = nil
1540
1542
  end
@@ -1575,12 +1577,6 @@ class Parser
1575
1577
  sect_style = attributes[1]
1576
1578
  sect_id, sect_reftext, sect_title, sect_level, sect_atx = parse_section_title reader, document, attributes['id']
1577
1579
 
1578
- if sect_reftext
1579
- attributes['reftext'] = sect_reftext
1580
- else
1581
- sect_reftext = attributes['reftext']
1582
- end
1583
-
1584
1580
  if sect_style
1585
1581
  if book && sect_style == 'abstract'
1586
1582
  sect_name, sect_level = 'chapter', 1
@@ -1599,6 +1595,7 @@ class Parser
1599
1595
  sect_name = 'section'
1600
1596
  end
1601
1597
 
1598
+ attributes['reftext'] = sect_reftext if sect_reftext
1602
1599
  section = Section.new parent, sect_level
1603
1600
  section.id, section.title, section.sectname, section.source_location = sect_id, sect_title, sect_name, source_location
1604
1601
  if sect_special
@@ -1606,7 +1603,7 @@ class Parser
1606
1603
  if sect_numbered
1607
1604
  section.numbered = true
1608
1605
  elsif document.attributes['sectnums'] == 'all'
1609
- section.numbered = book && sect_level == 1 ? :chapter : true
1606
+ section.numbered = (book && sect_level == 1 ? :chapter : true)
1610
1607
  end
1611
1608
  elsif document.attributes['sectnums'] && sect_level > 0
1612
1609
  # NOTE a special section here is guaranteed to be nested in another section
@@ -1618,7 +1615,7 @@ class Parser
1618
1615
  # generate an ID if one was not embedded or specified as anchor above section title
1619
1616
  if (id = section.id || (section.id = (document.attributes.key? 'sectids') ? (generated_id = Section.generate_id section.title, document) : nil))
1620
1617
  # convert title to resolve attributes while in scope
1621
- section.title if sect_title.include? ATTR_REF_HEAD unless generated_id
1618
+ section.title unless generated_id || !(sect_title.include? ATTR_REF_HEAD)
1622
1619
  unless document.register :refs, [id, section]
1623
1620
  logger.warn message_with_context %(id assigned to section already in use: #{id}), source_location: (reader.cursor_at_line reader.lineno - (sect_atx ? 1 : 2))
1624
1621
  end
@@ -1637,9 +1634,8 @@ class Parser
1637
1634
  #
1638
1635
  # Returns the Integer section level if the Reader is positioned at a section title or nil otherwise
1639
1636
  def self.is_next_line_section?(reader, attributes)
1640
- if (style = attributes[1]) && (style == 'discrete' || style == 'float')
1641
- return
1642
- elsif Compliance.underline_style_section_titles
1637
+ return if (style = attributes[1]) && (style == 'discrete' || style == 'float')
1638
+ if Compliance.underline_style_section_titles
1643
1639
  next_lines = reader.peek_lines 2, style && style == 'comment'
1644
1640
  is_section_title?(next_lines[0] || '', next_lines[1])
1645
1641
  else
@@ -1785,38 +1781,31 @@ class Parser
1785
1781
  # parse_header_metadata(Reader.new data, nil, normalize: true)
1786
1782
  # # => { 'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
1787
1783
  # # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.' }
1788
- def self.parse_header_metadata(reader, document = nil)
1784
+ def self.parse_header_metadata reader, document = nil, retrieve = true
1789
1785
  doc_attrs = document && document.attributes
1790
1786
  # NOTE this will discard any comment lines, but not skip blank lines
1791
1787
  process_attribute_entries reader, document
1792
1788
 
1793
- metadata, implicit_author, implicit_authorinitials = implicit_authors = {}, nil, nil
1794
-
1795
1789
  if reader.has_more_lines? && !reader.next_line_empty?
1796
- unless (author_metadata = process_authors reader.read_line).empty?
1797
- if document
1798
- # apply header subs and assign to document
1799
- author_metadata.each do |key, val|
1800
- # NOTE the attributes substitution only applies for the email record
1801
- doc_attrs[key] = ::String === val ? (document.apply_header_subs val) : val unless doc_attrs.key? key
1802
- end
1803
-
1804
- implicit_author = doc_attrs['author']
1805
- implicit_authorinitials = doc_attrs['authorinitials']
1806
- implicit_authors = doc_attrs['authors']
1790
+ authorcount = (implicit_author_metadata = process_authors reader.read_line).delete 'authorcount'
1791
+ if document && (doc_attrs['authorcount'] = authorcount) > 0
1792
+ implicit_author_metadata.each do |key, val|
1793
+ # apply header subs and assign to document; attributes substitution only relevant for email
1794
+ doc_attrs[key] = document.apply_header_subs val unless doc_attrs.key? key
1807
1795
  end
1808
-
1809
- metadata = author_metadata
1796
+ implicit_author = doc_attrs['author']
1797
+ implicit_authorinitials = doc_attrs['authorinitials']
1798
+ implicit_authors = doc_attrs['authors']
1810
1799
  end
1800
+ implicit_author_metadata['authorcount'] = authorcount
1811
1801
 
1812
1802
  # NOTE this will discard any comment lines, but not skip blank lines
1813
1803
  process_attribute_entries reader, document
1814
1804
 
1815
- rev_metadata = {}
1816
-
1817
1805
  if reader.has_more_lines? && !reader.next_line_empty?
1818
1806
  rev_line = reader.read_line
1819
- if (match = RevisionInfoLineRx.match(rev_line))
1807
+ if (match = RevisionInfoLineRx.match rev_line)
1808
+ rev_metadata = {}
1820
1809
  rev_metadata['revnumber'] = match[1].rstrip if match[1]
1821
1810
  unless (component = match[2].strip).empty?
1822
1811
  # version must begin with 'v' if date is absent
@@ -1827,31 +1816,24 @@ class Parser
1827
1816
  end
1828
1817
  end
1829
1818
  rev_metadata['revremark'] = match[3].rstrip if match[3]
1819
+ if document && !rev_metadata.empty?
1820
+ # apply header subs and assign to document
1821
+ rev_metadata.each do |key, val|
1822
+ doc_attrs[key] = document.apply_header_subs val unless doc_attrs.key? key
1823
+ end
1824
+ end
1830
1825
  else
1831
1826
  # throw it back
1832
1827
  reader.unshift_line rev_line
1833
1828
  end
1834
1829
  end
1835
1830
 
1836
- unless rev_metadata.empty?
1837
- if document
1838
- # apply header subs and assign to document
1839
- rev_metadata.each do |key, val|
1840
- unless doc_attrs.key? key
1841
- doc_attrs[key] = document.apply_header_subs val
1842
- end
1843
- end
1844
- end
1845
-
1846
- metadata.update rev_metadata
1847
- end
1848
-
1849
1831
  # NOTE this will discard any comment lines, but not skip blank lines
1850
1832
  process_attribute_entries reader, document
1851
1833
 
1852
1834
  reader.skip_blank_lines
1853
1835
  else
1854
- author_metadata = {}
1836
+ implicit_author_metadata = {}
1855
1837
  end
1856
1838
 
1857
1839
  # process author attribute entries that override (or stand in for) the implicit author line
@@ -1868,7 +1850,7 @@ class Parser
1868
1850
  while doc_attrs.key? author_key
1869
1851
  # only use indexed author attribute if value is different
1870
1852
  # leaves corner case if line matches with underscores converted to spaces; use double space to force
1871
- if (author_override = doc_attrs[author_key]) == author_metadata[author_key]
1853
+ if (author_override = doc_attrs[author_key]) == implicit_author_metadata[author_key]
1872
1854
  authors << nil
1873
1855
  sparse = true
1874
1856
  else
@@ -1880,23 +1862,26 @@ class Parser
1880
1862
  if explicit
1881
1863
  # rebuild implicit author names to reparse
1882
1864
  authors.each_with_index do |author, idx|
1883
- unless author
1884
- authors[idx] = [
1885
- author_metadata[%(firstname_#{name_idx = idx + 1})],
1886
- author_metadata[%(middlename_#{name_idx})],
1887
- author_metadata[%(lastname_#{name_idx})]
1888
- ].compact.map {|it| it.tr ' ', '_' }.join ' '
1889
- end
1865
+ next if author
1866
+ authors[idx] = [
1867
+ implicit_author_metadata[%(firstname_#{name_idx = idx + 1})],
1868
+ implicit_author_metadata[%(middlename_#{name_idx})],
1869
+ implicit_author_metadata[%(lastname_#{name_idx})]
1870
+ ].compact.map {|it| it.tr ' ', '_' }.join ' '
1890
1871
  end if sparse
1891
1872
  # process as names only
1892
1873
  author_metadata = process_authors authors, true, false
1893
1874
  else
1894
- author_metadata = {}
1875
+ author_metadata = { 'authorcount' => 0 }
1895
1876
  end
1896
1877
  end
1897
1878
 
1898
- if author_metadata.empty?
1899
- metadata['authorcount'] ||= (doc_attrs['authorcount'] = 0)
1879
+ if author_metadata['authorcount'] == 0
1880
+ if authorcount
1881
+ author_metadata = nil
1882
+ else
1883
+ doc_attrs['authorcount'] = 0
1884
+ end
1900
1885
  else
1901
1886
  doc_attrs.update author_metadata
1902
1887
 
@@ -1907,7 +1892,7 @@ class Parser
1907
1892
  end
1908
1893
  end
1909
1894
 
1910
- metadata
1895
+ implicit_author_metadata.merge rev_metadata.to_h, author_metadata.to_h if retrieve
1911
1896
  end
1912
1897
 
1913
1898
  # Internal: Parse the author line into a Hash of author metadata
@@ -2069,6 +2054,7 @@ class Parser
2069
2054
  return true
2070
2055
  end
2071
2056
  end
2057
+ nil
2072
2058
  end
2073
2059
 
2074
2060
  # Process consecutive attribute entry lines, ignoring adjacent line comments and comment blocks.
@@ -2173,9 +2159,10 @@ class Parser
2173
2159
  #
2174
2160
  # Returns the String 0-index marker for this list item
2175
2161
  def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
2176
- if list_type == :ulist
2162
+ case list_type
2163
+ when :ulist
2177
2164
  marker
2178
- elsif list_type == :olist
2165
+ when :olist
2179
2166
  resolve_ordered_list_marker(marker, ordinal, validate, reader)[0]
2180
2167
  else # :colist
2181
2168
  '<1>'
@@ -2478,7 +2465,7 @@ class Parser
2478
2465
 
2479
2466
  if pos == :start
2480
2467
  if line.include? delimiter
2481
- spec_part, delimiter, rest = line.partition delimiter
2468
+ spec_part, _, rest = line.partition delimiter
2482
2469
  if (m = CellSpecStartRx.match spec_part)
2483
2470
  return [{}, rest] if m[0].empty?
2484
2471
  else
@@ -2487,14 +2474,12 @@ class Parser
2487
2474
  else
2488
2475
  return [nil, line]
2489
2476
  end
2490
- else # pos == :end
2491
- if (m = CellSpecEndRx.match line)
2492
- # NOTE return the line stripped of trailing whitespace if no cellspec is found in this case
2493
- return [{}, line.rstrip] if m[0].lstrip.empty?
2494
- rest = m.pre_match
2495
- else
2496
- return [{}, line]
2497
- end
2477
+ elsif (m = CellSpecEndRx.match line) # when pos == :end
2478
+ # NOTE return the line stripped of trailing whitespace if no cellspec is found in this case
2479
+ return [{}, line.rstrip] if m[0].lstrip.empty?
2480
+ rest = m.pre_match
2481
+ else
2482
+ return [{}, line]
2498
2483
  end
2499
2484
 
2500
2485
  spec = {}
@@ -2502,10 +2487,11 @@ class Parser
2502
2487
  colspec, rowspec = m[1].split '.'
2503
2488
  colspec = colspec.nil_or_empty? ? 1 : colspec.to_i
2504
2489
  rowspec = rowspec.nil_or_empty? ? 1 : rowspec.to_i
2505
- if m[2] == '+'
2490
+ case m[2]
2491
+ when '+'
2506
2492
  spec['colspan'] = colspec unless colspec == 1
2507
2493
  spec['rowspan'] = rowspec unless rowspec == 1
2508
- elsif m[2] == '*'
2494
+ when '*'
2509
2495
  spec['repeatcol'] = colspec unless colspec == 1
2510
2496
  end
2511
2497
  end
@@ -2530,7 +2516,7 @@ class Parser
2530
2516
  # Public: Parse the first positional attribute and assign named attributes
2531
2517
  #
2532
2518
  # Parse the first positional attribute to extract the style, role and id
2533
- # parts, assign the values to their cooresponding attribute keys and return
2519
+ # parts, assign the values to their corresponding attribute keys and return
2534
2520
  # the parsed style from the first positional attribute.
2535
2521
  #
2536
2522
  # attributes - The Hash of attributes to process and update
@@ -2570,7 +2556,7 @@ class Parser
2570
2556
  accum = ''
2571
2557
  name = :option
2572
2558
  else
2573
- accum = accum + c
2559
+ accum += c
2574
2560
  end
2575
2561
  end
2576
2562
 
@@ -2661,9 +2647,9 @@ class Parser
2661
2647
  if tab_size > 0 && lines.any? {|line| line.include? TAB }
2662
2648
  full_tab_space = ' ' * tab_size
2663
2649
  lines.map! do |line|
2664
- if line.empty?
2650
+ if line.empty? || (tab_idx = line.index TAB).nil?
2665
2651
  line
2666
- elsif (tab_idx = line.index TAB)
2652
+ else
2667
2653
  if tab_idx == 0
2668
2654
  leading_tabs = 0
2669
2655
  line.each_byte do |b|
@@ -2681,22 +2667,20 @@ class Parser
2681
2667
  if c == TAB
2682
2668
  # calculate how many spaces this tab represents, then replace tab with spaces
2683
2669
  if (offset = idx + spaces_added) % tab_size == 0
2684
- spaces_added += (tab_size - 1)
2685
- result = result + full_tab_space
2670
+ spaces_added += tab_size - 1
2671
+ result += full_tab_space
2686
2672
  else
2687
2673
  unless (spaces = tab_size - offset % tab_size) == 1
2688
- spaces_added += (spaces - 1)
2674
+ spaces_added += spaces - 1
2689
2675
  end
2690
- result = result + (' ' * spaces)
2676
+ result += ' ' * spaces
2691
2677
  end
2692
2678
  else
2693
- result = result + c
2679
+ result += c
2694
2680
  end
2695
2681
  idx += 1
2696
2682
  end
2697
2683
  result
2698
- else
2699
- line
2700
2684
  end
2701
2685
  end
2702
2686
  end