asciidoctor 2.0.15 → 2.0.17

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