asciidoctor 2.0.10 → 2.0.17

Sign up to get free protection for your applications and to get access to all the features.
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