asciidoctor 2.0.13 → 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 (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