asciidoctor 2.0.15 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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