asciidoctor 2.0.10 → 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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +294 -30
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +16 -20
  5. data/README-fr.adoc +15 -22
  6. data/README-jp.adoc +15 -26
  7. data/README-zh_CN.adoc +21 -25
  8. data/README.adoc +161 -138
  9. data/asciidoctor.gemspec +6 -13
  10. data/data/locale/attributes-ar.adoc +4 -3
  11. data/data/locale/attributes-be.adoc +23 -0
  12. data/data/locale/attributes-bg.adoc +4 -3
  13. data/data/locale/attributes-ca.adoc +6 -5
  14. data/data/locale/attributes-cs.adoc +4 -3
  15. data/data/locale/attributes-da.adoc +6 -5
  16. data/data/locale/attributes-de.adoc +4 -4
  17. data/data/locale/attributes-en.adoc +4 -4
  18. data/data/locale/attributes-es.adoc +6 -5
  19. data/data/locale/attributes-fa.adoc +4 -3
  20. data/data/locale/attributes-fi.adoc +4 -3
  21. data/data/locale/attributes-fr.adoc +8 -7
  22. data/data/locale/attributes-hu.adoc +4 -3
  23. data/data/locale/attributes-id.adoc +4 -3
  24. data/data/locale/attributes-it.adoc +6 -5
  25. data/data/locale/attributes-ja.adoc +4 -3
  26. data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
  27. data/data/locale/attributes-nb.adoc +4 -3
  28. data/data/locale/attributes-nl.adoc +6 -5
  29. data/data/locale/attributes-nn.adoc +4 -3
  30. data/data/locale/attributes-pl.adoc +8 -7
  31. data/data/locale/attributes-pt.adoc +6 -5
  32. data/data/locale/attributes-pt_BR.adoc +6 -5
  33. data/data/locale/attributes-ro.adoc +4 -3
  34. data/data/locale/attributes-ru.adoc +6 -5
  35. data/data/locale/attributes-sr.adoc +4 -4
  36. data/data/locale/attributes-sr_Latn.adoc +4 -4
  37. data/data/locale/attributes-sv.adoc +4 -4
  38. data/data/locale/attributes-th.adoc +23 -0
  39. data/data/locale/attributes-tr.adoc +4 -3
  40. data/data/locale/attributes-uk.adoc +6 -5
  41. data/data/locale/attributes-vi.adoc +23 -0
  42. data/data/locale/attributes-zh_CN.adoc +4 -3
  43. data/data/locale/attributes-zh_TW.adoc +4 -3
  44. data/data/reference/syntax.adoc +14 -7
  45. data/data/stylesheets/asciidoctor-default.css +76 -76
  46. data/data/stylesheets/coderay-asciidoctor.css +9 -9
  47. data/lib/asciidoctor/abstract_block.rb +20 -13
  48. data/lib/asciidoctor/abstract_node.rb +23 -12
  49. data/lib/asciidoctor/attribute_list.rb +64 -72
  50. data/lib/asciidoctor/block.rb +6 -6
  51. data/lib/asciidoctor/cli/invoker.rb +3 -2
  52. data/lib/asciidoctor/cli/options.rb +32 -31
  53. data/lib/asciidoctor/convert.rb +168 -162
  54. data/lib/asciidoctor/converter/docbook5.rb +49 -34
  55. data/lib/asciidoctor/converter/html5.rb +180 -139
  56. data/lib/asciidoctor/converter/manpage.rb +118 -90
  57. data/lib/asciidoctor/converter/template.rb +15 -13
  58. data/lib/asciidoctor/converter.rb +19 -16
  59. data/lib/asciidoctor/core_ext/hash/merge.rb +1 -1
  60. data/lib/asciidoctor/document.rb +77 -86
  61. data/lib/asciidoctor/extensions.rb +22 -16
  62. data/lib/asciidoctor/helpers.rb +20 -15
  63. data/lib/asciidoctor/list.rb +2 -6
  64. data/lib/asciidoctor/load.rb +103 -101
  65. data/lib/asciidoctor/logging.rb +10 -8
  66. data/lib/asciidoctor/parser.rb +211 -220
  67. data/lib/asciidoctor/path_resolver.rb +17 -15
  68. data/lib/asciidoctor/reader.rb +87 -79
  69. data/lib/asciidoctor/rx.rb +9 -7
  70. data/lib/asciidoctor/section.rb +7 -0
  71. data/lib/asciidoctor/substitutors.rb +167 -148
  72. data/lib/asciidoctor/syntax_highlighter/coderay.rb +3 -2
  73. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +13 -5
  74. data/lib/asciidoctor/syntax_highlighter/prettify.rb +7 -4
  75. data/lib/asciidoctor/syntax_highlighter/pygments.rb +19 -11
  76. data/lib/asciidoctor/syntax_highlighter/rouge.rb +35 -20
  77. data/lib/asciidoctor/syntax_highlighter.rb +16 -16
  78. data/lib/asciidoctor/table.rb +70 -43
  79. data/lib/asciidoctor/timings.rb +3 -3
  80. data/lib/asciidoctor/version.rb +1 -1
  81. data/lib/asciidoctor.rb +45 -19
  82. data/man/asciidoctor.1 +29 -31
  83. data/man/asciidoctor.adoc +35 -29
  84. metadata +17 -70
@@ -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,14 +117,15 @@ 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
- block_attrs = parse_block_metadata_lines reader, document
122
+ block_attrs = reader.skip_blank_lines ? (parse_block_metadata_lines reader, document) : {}
123
123
  doc_attrs = document.attributes
124
124
 
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
@@ -215,9 +230,6 @@ class Parser
215
230
  name_section = initialize_section reader, document, {}
216
231
  name_section_buffer = (reader.read_lines_until break_on_blank_lines: true, skip_line_comments: true).map {|l| l.lstrip }.join ' '
217
232
  if ManpageNamePurposeRx =~ name_section_buffer
218
- doc_attrs['manname-title'] ||= name_section.title
219
- doc_attrs['manname-id'] = name_section.id if name_section.id
220
- doc_attrs['manpurpose'] = $2
221
233
  if (manname = $1).include? ATTR_REF_HEAD
222
234
  manname = document.sub_attributes manname
223
235
  end
@@ -226,8 +238,14 @@ class Parser
226
238
  else
227
239
  mannames = [manname]
228
240
  end
241
+ if (manpurpose = $2).include? ATTR_REF_HEAD
242
+ manpurpose = document.sub_attributes manpurpose
243
+ end
244
+ doc_attrs['manname-title'] ||= name_section.title
245
+ doc_attrs['manname-id'] = name_section.id if name_section.id
229
246
  doc_attrs['manname'] = manname
230
247
  doc_attrs['mannames'] = mannames
248
+ doc_attrs['manpurpose'] = manpurpose
231
249
  if document.backend == 'manpage'
232
250
  doc_attrs['docname'] = manname
233
251
  doc_attrs['outfilesuffix'] = %(.#{manvolnum})
@@ -327,7 +345,7 @@ class Parser
327
345
  if current_level == 0
328
346
  part = book
329
347
  elsif current_level == 1 && section.special
330
- # NOTE technically preface and abstract sections are only permitted in the book doctype
348
+ # NOTE technically preface sections are only permitted in the book doctype
331
349
  unless (sectname = section.sectname) == 'appendix' || sectname == 'preface' || sectname == 'abstract'
332
350
  expected_next_level = nil
333
351
  end
@@ -415,9 +433,6 @@ class Parser
415
433
 
416
434
  (intro || section).blocks << new_block
417
435
  attributes.clear
418
- #else
419
- # # don't clear attributes if we don't find a block because they may
420
- # # be trailing attributes that didn't get associated with a block
421
436
  end
422
437
  end
423
438
 
@@ -433,8 +448,10 @@ class Parser
433
448
  # is treated like an untitled section
434
449
  elsif preamble # implies parent == document
435
450
  if preamble.blocks?
451
+ if book || document.blocks[1] || !Compliance.unwrap_standalone_preamble
452
+ preamble.source_location = preamble.blocks[0].source_location if document.sourcemap
436
453
  # unwrap standalone preamble (i.e., document has no sections) except for books, if permissible
437
- unless book || document.blocks[1] || !Compliance.unwrap_standalone_preamble
454
+ else
438
455
  document.blocks.shift
439
456
  while (child_block = preamble.blocks.shift)
440
457
  document << child_block
@@ -447,10 +464,10 @@ class Parser
447
464
  end
448
465
 
449
466
  # The attributes returned here are orphaned attributes that fall at the end
450
- # 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
451
468
  # see "trailing block attributes transfer to the following section" in
452
469
  # test/attributes_test.rb for an example
453
- [section != parent ? section : nil, attributes.merge]
470
+ [section == parent ? nil : section, attributes.merge]
454
471
  end
455
472
 
456
473
  # Public: Parse and return the next Block at the Reader's current location
@@ -642,7 +659,7 @@ class Parser
642
659
  if (default_attrs = ext_config[:default_attrs])
643
660
  attributes.update(default_attrs) {|_, old_v| old_v }
644
661
  end
645
- if (block = extension.process_method[parent, target, attributes])
662
+ if (block = extension.process_method[parent, target, attributes]) && block != parent
646
663
  attributes.replace block.attributes
647
664
  break
648
665
  else
@@ -818,8 +835,8 @@ class Parser
818
835
  if comma_idx > 0
819
836
  language = (language.slice 0, comma_idx).strip
820
837
  attributes['linenums'] = '' if comma_idx < ll - 4
821
- else
822
- attributes['linenums'] = '' if ll > 4
838
+ elsif ll > 4
839
+ attributes['linenums'] = ''
823
840
  end
824
841
  else
825
842
  language = language.lstrip
@@ -858,6 +875,7 @@ class Parser
858
875
  when :literal
859
876
  block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
860
877
  when :example
878
+ attributes['caption'] = '' if attributes['collapsible-option']
861
879
  block = build_block(block_context, :compound, terminator, parent, reader, attributes)
862
880
  when :quote, :verse
863
881
  AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
@@ -899,9 +917,7 @@ class Parser
899
917
  # FIXME title and caption should be assigned when block is constructed (though we need to handle all cases)
900
918
  if attributes['title']
901
919
  block.title = block_title = attributes.delete 'title'
902
- if (caption_attr_name = CAPTION_ATTR_NAMES[block.context]) && document.attributes[caption_attr_name]
903
- block.assign_caption (attributes.delete 'caption')
904
- end
920
+ block.assign_caption attributes.delete 'caption' if CAPTION_ATTRIBUTE_NAMES[block.context]
905
921
  end
906
922
  # TODO eventually remove the style attribute from the attributes hash
907
923
  #block.style = attributes.delete 'style'
@@ -963,17 +979,12 @@ class Parser
963
979
  # special case for fenced code blocks
964
980
  if Compliance.markdown_syntax && (tip.start_with? '`')
965
981
  if tip_len == 4
966
- if tip == '````'
967
- return
968
- elsif (tip = tip.chop) == '```'
969
- line = tip
970
- line_len = tip_len = 3
971
- else
982
+ if tip == '````' || (tip = tip.chop) != '```'
972
983
  return
973
984
  end
974
- elsif tip == '```'
975
- # keep it
976
- else
985
+ line = tip
986
+ line_len = tip_len = 3
987
+ elsif tip != '```'
977
988
  return
978
989
  end
979
990
  elsif tip_len == 3
@@ -991,9 +1002,10 @@ class Parser
991
1002
  # if terminator is false, that means the all the lines in the reader should be parsed
992
1003
  # NOTE could invoke filter in here, before and after parsing
993
1004
  def self.build_block(block_context, content_model, terminator, parent, reader, attributes, options = {})
994
- if content_model == :skip
1005
+ case content_model
1006
+ when :skip
995
1007
  skip_processing, parse_as_content_model = true, :simple
996
- elsif content_model == :raw
1008
+ when :raw
997
1009
  skip_processing, parse_as_content_model = false, :simple
998
1010
  else
999
1011
  skip_processing, parse_as_content_model = false, content_model
@@ -1022,14 +1034,15 @@ class Parser
1022
1034
  block_reader = Reader.new reader.read_lines_until(terminator: terminator, skip_processing: skip_processing, context: block_context, cursor: :at_mark), block_cursor
1023
1035
  end
1024
1036
 
1025
- if content_model == :verbatim
1037
+ case content_model
1038
+ when :verbatim
1026
1039
  tab_size = (attributes['tabsize'] || parent.document.attributes['tabsize']).to_i
1027
1040
  if (indent = attributes['indent'])
1028
1041
  adjust_indentation! lines, indent.to_i, tab_size
1029
1042
  elsif tab_size > 0
1030
1043
  adjust_indentation! lines, -1, tab_size
1031
1044
  end
1032
- elsif content_model == :skip
1045
+ when :skip
1033
1046
  # QUESTION should we still invoke process method if extension is specified?
1034
1047
  return
1035
1048
  end
@@ -1037,12 +1050,12 @@ class Parser
1037
1050
  if (extension = options[:extension])
1038
1051
  # QUESTION do we want to delete the style?
1039
1052
  attributes.delete('style')
1040
- 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
1041
1054
  attributes.replace block.attributes
1042
1055
  # FIXME if the content model is set to compound, but we only have simple in this context, then
1043
1056
  # forcefully set the content_model to simple to prevent parsing blocks from children
1044
1057
  # TODO document this behavior!!
1045
- if block.content_model == :compound && !(lines = block.lines).empty?
1058
+ if block.content_model == :compound && Block === block && !(lines = block.lines).empty?
1046
1059
  content_model = :compound
1047
1060
  block_reader = Reader.new lines
1048
1061
  end
@@ -1148,14 +1161,16 @@ class Parser
1148
1161
  def self.catalog_inline_anchors text, block, document, reader
1149
1162
  text.scan InlineAnchorScanRx do
1150
1163
  if (id = $1)
1151
- if (reftext = $2)
1152
- next if (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1153
- end
1164
+ next if (reftext = $2) && (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1154
1165
  else
1155
1166
  id = $3
1156
1167
  if (reftext = $4)
1157
- reftext = reftext.gsub '\]', ']' if reftext.include? ']'
1158
- next if (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1168
+ if reftext.include? ']'
1169
+ reftext = reftext.gsub '\]', ']'
1170
+ reftext = document.sub_attributes reftext if reftext.include? ATTR_REF_HEAD
1171
+ elsif (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1172
+ next
1173
+ end
1159
1174
  end
1160
1175
  end
1161
1176
  unless document.register :refs, [id, (Inline.new block, :anchor, reftext, type: :ref, id: id)]
@@ -1288,7 +1303,8 @@ class Parser
1288
1303
  has_text = true
1289
1304
  list_item = ListItem.new(list_block, (item_text = match[2]))
1290
1305
  list_item.source_location = reader.cursor if list_block.document.sourcemap
1291
- if list_type == :ulist
1306
+ case list_type
1307
+ when :ulist
1292
1308
  list_item.marker = sibling_trait
1293
1309
  if item_text.start_with?('[')
1294
1310
  if style && style == 'bibliography'
@@ -1306,13 +1322,13 @@ class Parser
1306
1322
  list_item.text = item_text.slice(4, item_text.length)
1307
1323
  end
1308
1324
  end
1309
- elsif list_type == :olist
1325
+ when :olist
1310
1326
  sibling_trait, implicit_style = resolve_ordered_list_marker(sibling_trait, (ordinal = list_block.items.size), true, reader)
1311
1327
  list_item.marker = sibling_trait
1312
1328
  if ordinal == 0 && !style
1313
1329
  # using list level makes more sense, but we don't track it
1314
- # basing style on marker level is compliant with AsciiDoc Python
1315
- 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
1316
1332
  end
1317
1333
  if item_text.start_with?('[[') && LeadingInlineAnchorRx =~ item_text
1318
1334
  catalog_inline_anchor $1, $2, list_item, reader
@@ -1438,95 +1454,89 @@ class Parser
1438
1454
  # FIXME to be AsciiDoc compliant, we shouldn't break if style in attribute line is "literal" (i.e., [literal])
1439
1455
  elsif dlist && continuation != :active && (BlockAttributeLineRx.match? this_line)
1440
1456
  break
1441
- else
1442
- if continuation == :active && !this_line.empty?
1443
- # literal paragraphs have special considerations (and this is one of
1444
- # two entry points into one)
1445
- # if we don't process it as a whole, then a line in it that looks like a
1446
- # list item will throw off the exit from it
1447
- if LiteralParagraphRx.match? this_line
1448
- reader.unshift_line this_line
1449
- if dlist
1450
- # we may be in an indented list disguised as a literal paragraph
1451
- # so we need to make sure we don't slurp up a legitimate sibling
1452
- 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 }
1453
- else
1454
- buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
1455
- end
1456
- continuation = :inactive
1457
- # let block metadata play out until we find the block
1458
- elsif (BlockTitleRx.match? this_line) || (BlockAttributeLineRx.match? this_line) || (AttributeEntryRx.match? this_line)
1459
- 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 }
1460
1468
  else
1461
- if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx].match? this_line }
1462
- within_nested_list = true
1463
- if nested_list_type == :dlist && $3.nil_or_empty?
1464
- # get greedy again
1465
- has_text = false
1466
- end
1467
- end
1468
- buffer << this_line
1469
- continuation = :inactive
1469
+ buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
1470
1470
  end
1471
- elsif prev_line && prev_line.empty?
1472
- # advance to the next line of content
1473
- if this_line.empty?
1474
- # stop reading if we reach eof
1475
- break unless (this_line = reader.skip_blank_lines && reader.read_line)
1476
- # stop reading if we hit a sibling list item
1477
- 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
1478
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
1479
1494
 
1480
- if this_line == LIST_CONTINUATION
1481
- 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 })
1482
1504
  buffer << this_line
1483
- else
1484
- # has_text is only relevant for dlist, which is more greedy until it has text for an item
1485
- # for all other lists, has_text is always true
1486
- # in this block, we have to see whether we stay in the list
1487
- if has_text
1488
- # TODO any way to combine this with the check after skipping blank lines?
1489
- if is_sibling_list_item?(this_line, list_type, sibling_trait)
1490
- break
1491
- elsif nested_list_type = NESTABLE_LIST_CONTEXTS.find {|ctx| ListRxMap[ctx] =~ this_line }
1492
- buffer << this_line
1493
- within_nested_list = true
1494
- if nested_list_type == :dlist && $3.nil_or_empty?
1495
- # get greedy again
1496
- has_text = false
1497
- end
1498
- # slurp up any literal paragraph offset by blank lines
1499
- # NOTE we have to check for indented list items first
1500
- elsif LiteralParagraphRx.match? this_line
1501
- reader.unshift_line this_line
1502
- if dlist
1503
- # we may be in an indented list disguised as a literal paragraph
1504
- # so we need to make sure we don't slurp up a legitimate sibling
1505
- 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 }
1506
- else
1507
- buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
1508
- end
1509
- else
1510
- break
1511
- end
1512
- else # only dlist in need of item text, so slurp it up!
1513
- # pop the blank line so it's not interpretted as a list continuation
1514
- buffer.pop unless within_nested_list
1515
- buffer << this_line
1516
- has_text = true
1517
- end
1518
- end
1519
- else
1520
- has_text = true unless this_line.empty?
1521
- if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx] =~ this_line }
1522
1505
  within_nested_list = true
1523
1506
  if nested_list_type == :dlist && $3.nil_or_empty?
1524
1507
  # get greedy again
1525
1508
  has_text = false
1526
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
1527
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
1528
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
1529
1538
  end
1539
+ buffer << this_line
1530
1540
  end
1531
1541
  this_line = nil
1532
1542
  end
@@ -1567,12 +1577,6 @@ class Parser
1567
1577
  sect_style = attributes[1]
1568
1578
  sect_id, sect_reftext, sect_title, sect_level, sect_atx = parse_section_title reader, document, attributes['id']
1569
1579
 
1570
- if sect_reftext
1571
- attributes['reftext'] = sect_reftext
1572
- else
1573
- sect_reftext = attributes['reftext']
1574
- end
1575
-
1576
1580
  if sect_style
1577
1581
  if book && sect_style == 'abstract'
1578
1582
  sect_name, sect_level = 'chapter', 1
@@ -1591,6 +1595,7 @@ class Parser
1591
1595
  sect_name = 'section'
1592
1596
  end
1593
1597
 
1598
+ attributes['reftext'] = sect_reftext if sect_reftext
1594
1599
  section = Section.new parent, sect_level
1595
1600
  section.id, section.title, section.sectname, section.source_location = sect_id, sect_title, sect_name, source_location
1596
1601
  if sect_special
@@ -1598,7 +1603,7 @@ class Parser
1598
1603
  if sect_numbered
1599
1604
  section.numbered = true
1600
1605
  elsif document.attributes['sectnums'] == 'all'
1601
- section.numbered = book && sect_level == 1 ? :chapter : true
1606
+ section.numbered = (book && sect_level == 1 ? :chapter : true)
1602
1607
  end
1603
1608
  elsif document.attributes['sectnums'] && sect_level > 0
1604
1609
  # NOTE a special section here is guaranteed to be nested in another section
@@ -1610,7 +1615,7 @@ class Parser
1610
1615
  # generate an ID if one was not embedded or specified as anchor above section title
1611
1616
  if (id = section.id || (section.id = (document.attributes.key? 'sectids') ? (generated_id = Section.generate_id section.title, document) : nil))
1612
1617
  # convert title to resolve attributes while in scope
1613
- section.title if sect_title.include? ATTR_REF_HEAD unless generated_id
1618
+ section.title unless generated_id || !(sect_title.include? ATTR_REF_HEAD)
1614
1619
  unless document.register :refs, [id, section]
1615
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))
1616
1621
  end
@@ -1629,9 +1634,8 @@ class Parser
1629
1634
  #
1630
1635
  # Returns the Integer section level if the Reader is positioned at a section title or nil otherwise
1631
1636
  def self.is_next_line_section?(reader, attributes)
1632
- if (style = attributes[1]) && (style == 'discrete' || style == 'float')
1633
- return
1634
- elsif Compliance.underline_style_section_titles
1637
+ return if (style = attributes[1]) && (style == 'discrete' || style == 'float')
1638
+ if Compliance.underline_style_section_titles
1635
1639
  next_lines = reader.peek_lines 2, style && style == 'comment'
1636
1640
  is_section_title?(next_lines[0] || '', next_lines[1])
1637
1641
  else
@@ -1777,38 +1781,31 @@ class Parser
1777
1781
  # parse_header_metadata(Reader.new data, nil, normalize: true)
1778
1782
  # # => { 'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
1779
1783
  # # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.' }
1780
- def self.parse_header_metadata(reader, document = nil)
1784
+ def self.parse_header_metadata reader, document = nil, retrieve = true
1781
1785
  doc_attrs = document && document.attributes
1782
1786
  # NOTE this will discard any comment lines, but not skip blank lines
1783
1787
  process_attribute_entries reader, document
1784
1788
 
1785
- metadata, implicit_author, implicit_authorinitials = implicit_authors = {}, nil, nil
1786
-
1787
1789
  if reader.has_more_lines? && !reader.next_line_empty?
1788
- unless (author_metadata = process_authors reader.read_line).empty?
1789
- if document
1790
- # apply header subs and assign to document
1791
- author_metadata.each do |key, val|
1792
- # NOTE the attributes substitution only applies for the email record
1793
- doc_attrs[key] = ::String === val ? (document.apply_header_subs val) : val unless doc_attrs.key? key
1794
- end
1795
-
1796
- implicit_author = doc_attrs['author']
1797
- implicit_authorinitials = doc_attrs['authorinitials']
1798
- 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
1799
1795
  end
1800
-
1801
- metadata = author_metadata
1796
+ implicit_author = doc_attrs['author']
1797
+ implicit_authorinitials = doc_attrs['authorinitials']
1798
+ implicit_authors = doc_attrs['authors']
1802
1799
  end
1800
+ implicit_author_metadata['authorcount'] = authorcount
1803
1801
 
1804
1802
  # NOTE this will discard any comment lines, but not skip blank lines
1805
1803
  process_attribute_entries reader, document
1806
1804
 
1807
- rev_metadata = {}
1808
-
1809
1805
  if reader.has_more_lines? && !reader.next_line_empty?
1810
1806
  rev_line = reader.read_line
1811
- if (match = RevisionInfoLineRx.match(rev_line))
1807
+ if (match = RevisionInfoLineRx.match rev_line)
1808
+ rev_metadata = {}
1812
1809
  rev_metadata['revnumber'] = match[1].rstrip if match[1]
1813
1810
  unless (component = match[2].strip).empty?
1814
1811
  # version must begin with 'v' if date is absent
@@ -1819,31 +1816,24 @@ class Parser
1819
1816
  end
1820
1817
  end
1821
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
1822
1825
  else
1823
1826
  # throw it back
1824
1827
  reader.unshift_line rev_line
1825
1828
  end
1826
1829
  end
1827
1830
 
1828
- unless rev_metadata.empty?
1829
- if document
1830
- # apply header subs and assign to document
1831
- rev_metadata.each do |key, val|
1832
- unless doc_attrs.key? key
1833
- doc_attrs[key] = document.apply_header_subs val
1834
- end
1835
- end
1836
- end
1837
-
1838
- metadata.update rev_metadata
1839
- end
1840
-
1841
1831
  # NOTE this will discard any comment lines, but not skip blank lines
1842
1832
  process_attribute_entries reader, document
1843
1833
 
1844
1834
  reader.skip_blank_lines
1845
1835
  else
1846
- author_metadata = {}
1836
+ implicit_author_metadata = {}
1847
1837
  end
1848
1838
 
1849
1839
  # process author attribute entries that override (or stand in for) the implicit author line
@@ -1860,7 +1850,7 @@ class Parser
1860
1850
  while doc_attrs.key? author_key
1861
1851
  # only use indexed author attribute if value is different
1862
1852
  # leaves corner case if line matches with underscores converted to spaces; use double space to force
1863
- if (author_override = doc_attrs[author_key]) == author_metadata[author_key]
1853
+ if (author_override = doc_attrs[author_key]) == implicit_author_metadata[author_key]
1864
1854
  authors << nil
1865
1855
  sparse = true
1866
1856
  else
@@ -1872,23 +1862,26 @@ class Parser
1872
1862
  if explicit
1873
1863
  # rebuild implicit author names to reparse
1874
1864
  authors.each_with_index do |author, idx|
1875
- unless author
1876
- authors[idx] = [
1877
- author_metadata[%(firstname_#{name_idx = idx + 1})],
1878
- author_metadata[%(middlename_#{name_idx})],
1879
- author_metadata[%(lastname_#{name_idx})]
1880
- ].compact.map {|it| it.tr ' ', '_' }.join ' '
1881
- 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 ' '
1882
1871
  end if sparse
1883
1872
  # process as names only
1884
1873
  author_metadata = process_authors authors, true, false
1885
1874
  else
1886
- author_metadata = {}
1875
+ author_metadata = { 'authorcount' => 0 }
1887
1876
  end
1888
1877
  end
1889
1878
 
1890
- if author_metadata.empty?
1891
- 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
1892
1885
  else
1893
1886
  doc_attrs.update author_metadata
1894
1887
 
@@ -1899,7 +1892,7 @@ class Parser
1899
1892
  end
1900
1893
  end
1901
1894
 
1902
- metadata
1895
+ implicit_author_metadata.merge rev_metadata.to_h, author_metadata.to_h if retrieve
1903
1896
  end
1904
1897
 
1905
1898
  # Internal: Parse the author line into a Hash of author metadata
@@ -2061,6 +2054,7 @@ class Parser
2061
2054
  return true
2062
2055
  end
2063
2056
  end
2057
+ nil
2064
2058
  end
2065
2059
 
2066
2060
  # Process consecutive attribute entry lines, ignoring adjacent line comments and comment blocks.
@@ -2119,6 +2113,8 @@ class Parser
2119
2113
  name = 'sectnums'
2120
2114
  elsif name == 'hardbreaks'
2121
2115
  name = 'hardbreaks-option'
2116
+ elsif name == 'showtitle'
2117
+ store_attribute 'notitle', (value ? nil : ''), doc, attrs
2122
2118
  end
2123
2119
 
2124
2120
  if doc
@@ -2163,9 +2159,10 @@ class Parser
2163
2159
  #
2164
2160
  # Returns the String 0-index marker for this list item
2165
2161
  def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
2166
- if list_type == :ulist
2162
+ case list_type
2163
+ when :ulist
2167
2164
  marker
2168
- elsif list_type == :olist
2165
+ when :olist
2169
2166
  resolve_ordered_list_marker(marker, ordinal, validate, reader)[0]
2170
2167
  else # :colist
2171
2168
  '<1>'
@@ -2273,9 +2270,15 @@ class Parser
2273
2270
  end
2274
2271
 
2275
2272
  skipped = table_reader.skip_blank_lines || 0
2273
+ if attributes['header-option']
2274
+ table.has_header_option = true
2275
+ elsif skipped == 0 && !attributes['noheader-option']
2276
+ # NOTE: assume table has header until we know otherwise; if it doesn't (nil), cells in first row get reprocessed
2277
+ table.has_header_option = :implicit
2278
+ implicit_header = true
2279
+ end
2276
2280
  parser_ctx = Table::ParserContext.new table_reader, table, attributes
2277
2281
  format, loop_idx, implicit_header_boundary = parser_ctx.format, -1, nil
2278
- implicit_header = true unless skipped > 0 || attributes['header-option'] || attributes['noheader-option']
2279
2282
 
2280
2283
  while (line = table_reader.read_line)
2281
2284
  if (beyond_first = (loop_idx += 1) > 0) && line.empty?
@@ -2295,7 +2298,7 @@ class Parser
2295
2298
  implicit_header_boundary = nil if implicit_header_boundary
2296
2299
  # otherwise, the cell continues from previous line
2297
2300
  elsif implicit_header_boundary && implicit_header_boundary == loop_idx
2298
- implicit_header, implicit_header_boundary = false, nil
2301
+ table.has_header_option = implicit_header = implicit_header_boundary = nil
2299
2302
  end
2300
2303
  end
2301
2304
  end
@@ -2307,7 +2310,7 @@ class Parser
2307
2310
  if table_reader.has_more_lines? && table_reader.peek_line.empty?
2308
2311
  implicit_header_boundary = 1
2309
2312
  else
2310
- implicit_header = false
2313
+ table.has_header_option = implicit_header = nil
2311
2314
  end
2312
2315
  end
2313
2316
  end
@@ -2358,7 +2361,7 @@ class Parser
2358
2361
  case format
2359
2362
  when 'csv'
2360
2363
  if parser_ctx.buffer_has_unclosed_quotes?
2361
- implicit_header, implicit_header_boundary = false, nil if implicit_header_boundary && loop_idx == 0
2364
+ table.has_header_option = implicit_header = implicit_header_boundary = nil if implicit_header_boundary && loop_idx == 0
2362
2365
  parser_ctx.keep_cell_open
2363
2366
  else
2364
2367
  parser_ctx.close_cell true
@@ -2380,15 +2383,8 @@ class Parser
2380
2383
  end
2381
2384
  end
2382
2385
 
2383
- unless (table.attributes['colcount'] ||= table.columns.size) == 0 || explicit_colspecs
2384
- table.assign_column_widths
2385
- end
2386
-
2387
- if implicit_header
2388
- table.has_header_option = true
2389
- attributes['header-option'] = ''
2390
- end
2391
-
2386
+ table.assign_column_widths unless (table.attributes['colcount'] ||= table.columns.size) == 0 || explicit_colspecs
2387
+ table.has_header_option = true if implicit_header
2392
2388
  table.partition_header_footer attributes
2393
2389
 
2394
2390
  table
@@ -2469,7 +2465,7 @@ class Parser
2469
2465
 
2470
2466
  if pos == :start
2471
2467
  if line.include? delimiter
2472
- spec_part, delimiter, rest = line.partition delimiter
2468
+ spec_part, _, rest = line.partition delimiter
2473
2469
  if (m = CellSpecStartRx.match spec_part)
2474
2470
  return [{}, rest] if m[0].empty?
2475
2471
  else
@@ -2478,14 +2474,12 @@ class Parser
2478
2474
  else
2479
2475
  return [nil, line]
2480
2476
  end
2481
- else # pos == :end
2482
- if (m = CellSpecEndRx.match line)
2483
- # NOTE return the line stripped of trailing whitespace if no cellspec is found in this case
2484
- return [{}, line.rstrip] if m[0].lstrip.empty?
2485
- rest = m.pre_match
2486
- else
2487
- return [{}, line]
2488
- 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]
2489
2483
  end
2490
2484
 
2491
2485
  spec = {}
@@ -2493,10 +2487,11 @@ class Parser
2493
2487
  colspec, rowspec = m[1].split '.'
2494
2488
  colspec = colspec.nil_or_empty? ? 1 : colspec.to_i
2495
2489
  rowspec = rowspec.nil_or_empty? ? 1 : rowspec.to_i
2496
- if m[2] == '+'
2490
+ case m[2]
2491
+ when '+'
2497
2492
  spec['colspan'] = colspec unless colspec == 1
2498
2493
  spec['rowspan'] = rowspec unless rowspec == 1
2499
- elsif m[2] == '*'
2494
+ when '*'
2500
2495
  spec['repeatcol'] = colspec unless colspec == 1
2501
2496
  end
2502
2497
  end
@@ -2521,7 +2516,7 @@ class Parser
2521
2516
  # Public: Parse the first positional attribute and assign named attributes
2522
2517
  #
2523
2518
  # Parse the first positional attribute to extract the style, role and id
2524
- # parts, assign the values to their cooresponding attribute keys and return
2519
+ # parts, assign the values to their corresponding attribute keys and return
2525
2520
  # the parsed style from the first positional attribute.
2526
2521
  #
2527
2522
  # attributes - The Hash of attributes to process and update
@@ -2561,7 +2556,7 @@ class Parser
2561
2556
  accum = ''
2562
2557
  name = :option
2563
2558
  else
2564
- accum = accum + c
2559
+ accum += c
2565
2560
  end
2566
2561
  end
2567
2562
 
@@ -2579,9 +2574,7 @@ class Parser
2579
2574
  attributes['role'] = (existing_role = attributes['role']).nil_or_empty? ? (parsed_attrs[:role].join ' ') : %(#{existing_role} #{parsed_attrs[:role].join ' '})
2580
2575
  end
2581
2576
 
2582
- if parsed_attrs.key? :option
2583
- (opts = parsed_attrs[:option]).each {|opt| attributes[%(#{opt}-option)] = '' }
2584
- end
2577
+ parsed_attrs[:option].each {|opt| attributes[%(#{opt}-option)] = '' } if parsed_attrs.key? :option
2585
2578
 
2586
2579
  parsed_style
2587
2580
  else
@@ -2654,9 +2647,9 @@ class Parser
2654
2647
  if tab_size > 0 && lines.any? {|line| line.include? TAB }
2655
2648
  full_tab_space = ' ' * tab_size
2656
2649
  lines.map! do |line|
2657
- if line.empty?
2650
+ if line.empty? || (tab_idx = line.index TAB).nil?
2658
2651
  line
2659
- elsif (tab_idx = line.index TAB)
2652
+ else
2660
2653
  if tab_idx == 0
2661
2654
  leading_tabs = 0
2662
2655
  line.each_byte do |b|
@@ -2674,22 +2667,20 @@ class Parser
2674
2667
  if c == TAB
2675
2668
  # calculate how many spaces this tab represents, then replace tab with spaces
2676
2669
  if (offset = idx + spaces_added) % tab_size == 0
2677
- spaces_added += (tab_size - 1)
2678
- result = result + full_tab_space
2670
+ spaces_added += tab_size - 1
2671
+ result += full_tab_space
2679
2672
  else
2680
2673
  unless (spaces = tab_size - offset % tab_size) == 1
2681
- spaces_added += (spaces - 1)
2674
+ spaces_added += spaces - 1
2682
2675
  end
2683
- result = result + (' ' * spaces)
2676
+ result += ' ' * spaces
2684
2677
  end
2685
2678
  else
2686
- result = result + c
2679
+ result += c
2687
2680
  end
2688
2681
  idx += 1
2689
2682
  end
2690
2683
  result
2691
- else
2692
- line
2693
2684
  end
2694
2685
  end
2695
2686
  end