asciidoctor 2.0.9 → 2.0.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +193 -16
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +12 -13
  5. data/README-fr.adoc +11 -15
  6. data/README-jp.adoc +242 -185
  7. data/README-zh_CN.adoc +17 -18
  8. data/README.adoc +133 -131
  9. data/asciidoctor.gemspec +6 -6
  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 +6 -5
  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-tr.adoc +4 -3
  39. data/data/locale/attributes-uk.adoc +6 -5
  40. data/data/locale/attributes-zh_CN.adoc +4 -3
  41. data/data/locale/attributes-zh_TW.adoc +4 -3
  42. data/data/reference/syntax.adoc +14 -7
  43. data/data/stylesheets/asciidoctor-default.css +30 -30
  44. data/lib/asciidoctor.rb +40 -14
  45. data/lib/asciidoctor/abstract_block.rb +9 -4
  46. data/lib/asciidoctor/abstract_node.rb +16 -6
  47. data/lib/asciidoctor/attribute_list.rb +63 -71
  48. data/lib/asciidoctor/cli/invoker.rb +2 -0
  49. data/lib/asciidoctor/cli/options.rb +10 -9
  50. data/lib/asciidoctor/convert.rb +167 -162
  51. data/lib/asciidoctor/converter.rb +13 -12
  52. data/lib/asciidoctor/converter/docbook5.rb +5 -9
  53. data/lib/asciidoctor/converter/html5.rb +58 -45
  54. data/lib/asciidoctor/converter/manpage.rb +61 -38
  55. data/lib/asciidoctor/converter/template.rb +3 -0
  56. data/lib/asciidoctor/document.rb +44 -51
  57. data/lib/asciidoctor/extensions.rb +2 -4
  58. data/lib/asciidoctor/helpers.rb +20 -15
  59. data/lib/asciidoctor/load.rb +102 -101
  60. data/lib/asciidoctor/parser.rb +40 -32
  61. data/lib/asciidoctor/path_resolver.rb +14 -12
  62. data/lib/asciidoctor/reader.rb +20 -13
  63. data/lib/asciidoctor/rx.rb +7 -6
  64. data/lib/asciidoctor/substitutors.rb +69 -50
  65. data/lib/asciidoctor/syntax_highlighter.rb +15 -7
  66. data/lib/asciidoctor/syntax_highlighter/coderay.rb +1 -1
  67. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +12 -4
  68. data/lib/asciidoctor/syntax_highlighter/prettify.rb +7 -4
  69. data/lib/asciidoctor/syntax_highlighter/pygments.rb +6 -7
  70. data/lib/asciidoctor/syntax_highlighter/rouge.rb +33 -19
  71. data/lib/asciidoctor/table.rb +52 -23
  72. data/lib/asciidoctor/version.rb +1 -1
  73. data/man/asciidoctor.1 +8 -8
  74. data/man/asciidoctor.adoc +4 -4
  75. metadata +16 -15
@@ -119,7 +119,7 @@ class Parser
119
119
  # returns the Hash of orphan block attributes captured above the header
120
120
  def self.parse_document_header(reader, document)
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,
@@ -144,7 +144,10 @@ class Parser
144
144
  l0_section_title = nil
145
145
  else
146
146
  document.title = l0_section_title
147
- doc_attrs['doctitle'] = doctitle_attr_val = document.apply_header_subs l0_section_title
147
+ if (doc_attrs['doctitle'] = doctitle_attr_val = document.sub_specialchars l0_section_title).include? ATTR_REF_HEAD
148
+ # QUESTION should we defer substituting attributes until the end of the header? or should we substitute again if necessary?
149
+ doc_attrs['doctitle'] = doctitle_attr_val = document.sub_attributes doctitle_attr_val, attribute_missing: 'skip'
150
+ end
148
151
  end
149
152
  document.header.source_location = source_location if source_location
150
153
  # default to compat-mode if document has setext doctitle
@@ -215,9 +218,6 @@ class Parser
215
218
  name_section = initialize_section reader, document, {}
216
219
  name_section_buffer = (reader.read_lines_until break_on_blank_lines: true, skip_line_comments: true).map {|l| l.lstrip }.join ' '
217
220
  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
221
  if (manname = $1).include? ATTR_REF_HEAD
222
222
  manname = document.sub_attributes manname
223
223
  end
@@ -226,8 +226,14 @@ class Parser
226
226
  else
227
227
  mannames = [manname]
228
228
  end
229
+ if (manpurpose = $2).include? ATTR_REF_HEAD
230
+ manpurpose = document.sub_attributes manpurpose
231
+ end
232
+ doc_attrs['manname-title'] ||= name_section.title
233
+ doc_attrs['manname-id'] = name_section.id if name_section.id
229
234
  doc_attrs['manname'] = manname
230
235
  doc_attrs['mannames'] = mannames
236
+ doc_attrs['manpurpose'] = manpurpose
231
237
  if document.backend == 'manpage'
232
238
  doc_attrs['docname'] = manname
233
239
  doc_attrs['outfilesuffix'] = %(.#{manvolnum})
@@ -327,7 +333,7 @@ class Parser
327
333
  if current_level == 0
328
334
  part = book
329
335
  elsif current_level == 1 && section.special
330
- # NOTE technically preface and abstract sections are only permitted in the book doctype
336
+ # NOTE technically preface sections are only permitted in the book doctype
331
337
  unless (sectname = section.sectname) == 'appendix' || sectname == 'preface' || sectname == 'abstract'
332
338
  expected_next_level = nil
333
339
  end
@@ -433,8 +439,10 @@ class Parser
433
439
  # is treated like an untitled section
434
440
  elsif preamble # implies parent == document
435
441
  if preamble.blocks?
442
+ if book || document.blocks[1] || !Compliance.unwrap_standalone_preamble
443
+ preamble.source_location = preamble.blocks[0].source_location if document.sourcemap
436
444
  # unwrap standalone preamble (i.e., document has no sections) except for books, if permissible
437
- unless book || document.blocks[1] || !Compliance.unwrap_standalone_preamble
445
+ else
438
446
  document.blocks.shift
439
447
  while (child_block = preamble.blocks.shift)
440
448
  document << child_block
@@ -858,6 +866,7 @@ class Parser
858
866
  when :literal
859
867
  block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
860
868
  when :example
869
+ attributes['caption'] = '' if attributes['collapsible-option']
861
870
  block = build_block(block_context, :compound, terminator, parent, reader, attributes)
862
871
  when :quote, :verse
863
872
  AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
@@ -899,9 +908,7 @@ class Parser
899
908
  # FIXME title and caption should be assigned when block is constructed (though we need to handle all cases)
900
909
  if attributes['title']
901
910
  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
911
+ block.assign_caption (attributes.delete 'caption') if CAPTION_ATTRIBUTE_NAMES[block.context]
905
912
  end
906
913
  # TODO eventually remove the style attribute from the attributes hash
907
914
  #block.style = attributes.delete 'style'
@@ -1148,14 +1155,16 @@ class Parser
1148
1155
  def self.catalog_inline_anchors text, block, document, reader
1149
1156
  text.scan InlineAnchorScanRx do
1150
1157
  if (id = $1)
1151
- if (reftext = $2)
1152
- next if (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1153
- end
1158
+ next if (reftext = $2) && (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1154
1159
  else
1155
1160
  id = $3
1156
1161
  if (reftext = $4)
1157
- reftext = reftext.gsub '\]', ']' if reftext.include? ']'
1158
- next if (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1162
+ if reftext.include? ']'
1163
+ reftext = reftext.gsub '\]', ']'
1164
+ reftext = document.sub_attributes reftext if reftext.include? ATTR_REF_HEAD
1165
+ elsif (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1166
+ next
1167
+ end
1159
1168
  end
1160
1169
  end
1161
1170
  unless document.register :refs, [id, (Inline.new block, :anchor, reftext, type: :ref, id: id)]
@@ -1311,7 +1320,7 @@ class Parser
1311
1320
  list_item.marker = sibling_trait
1312
1321
  if ordinal == 0 && !style
1313
1322
  # using list level makes more sense, but we don't track it
1314
- # basing style on marker level is compliant with AsciiDoc Python
1323
+ # basing style on marker level is compliant with AsciiDoc.py
1315
1324
  list_block.style = implicit_style || ((ORDERED_LIST_STYLES[sibling_trait.length - 1] || 'arabic').to_s)
1316
1325
  end
1317
1326
  if item_text.start_with?('[[') && LeadingInlineAnchorRx =~ item_text
@@ -2119,6 +2128,8 @@ class Parser
2119
2128
  name = 'sectnums'
2120
2129
  elsif name == 'hardbreaks'
2121
2130
  name = 'hardbreaks-option'
2131
+ elsif name == 'showtitle'
2132
+ store_attribute 'notitle', (value ? nil : ''), doc, attrs
2122
2133
  end
2123
2134
 
2124
2135
  if doc
@@ -2273,9 +2284,15 @@ class Parser
2273
2284
  end
2274
2285
 
2275
2286
  skipped = table_reader.skip_blank_lines || 0
2287
+ if attributes['header-option']
2288
+ table.has_header_option = true
2289
+ elsif skipped == 0 && !attributes['noheader-option']
2290
+ # NOTE: assume table has header until we know otherwise; if it doesn't (nil), cells in first row get reprocessed
2291
+ table.has_header_option = :implicit
2292
+ implicit_header = true
2293
+ end
2276
2294
  parser_ctx = Table::ParserContext.new table_reader, table, attributes
2277
2295
  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
2296
 
2280
2297
  while (line = table_reader.read_line)
2281
2298
  if (beyond_first = (loop_idx += 1) > 0) && line.empty?
@@ -2295,7 +2312,7 @@ class Parser
2295
2312
  implicit_header_boundary = nil if implicit_header_boundary
2296
2313
  # otherwise, the cell continues from previous line
2297
2314
  elsif implicit_header_boundary && implicit_header_boundary == loop_idx
2298
- implicit_header, implicit_header_boundary = false, nil
2315
+ table.has_header_option = implicit_header = implicit_header_boundary = nil
2299
2316
  end
2300
2317
  end
2301
2318
  end
@@ -2307,7 +2324,7 @@ class Parser
2307
2324
  if table_reader.has_more_lines? && table_reader.peek_line.empty?
2308
2325
  implicit_header_boundary = 1
2309
2326
  else
2310
- implicit_header = false
2327
+ table.has_header_option = implicit_header = nil
2311
2328
  end
2312
2329
  end
2313
2330
  end
@@ -2358,7 +2375,7 @@ class Parser
2358
2375
  case format
2359
2376
  when 'csv'
2360
2377
  if parser_ctx.buffer_has_unclosed_quotes?
2361
- implicit_header, implicit_header_boundary = false, nil if implicit_header_boundary && loop_idx == 0
2378
+ table.has_header_option = implicit_header = implicit_header_boundary = nil if implicit_header_boundary && loop_idx == 0
2362
2379
  parser_ctx.keep_cell_open
2363
2380
  else
2364
2381
  parser_ctx.close_cell true
@@ -2380,15 +2397,8 @@ class Parser
2380
2397
  end
2381
2398
  end
2382
2399
 
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
-
2400
+ table.assign_column_widths unless (table.attributes['colcount'] ||= table.columns.size) == 0 || explicit_colspecs
2401
+ table.has_header_option = true if implicit_header
2392
2402
  table.partition_header_footer attributes
2393
2403
 
2394
2404
  table
@@ -2579,9 +2589,7 @@ class Parser
2579
2589
  attributes['role'] = (existing_role = attributes['role']).nil_or_empty? ? (parsed_attrs[:role].join ' ') : %(#{existing_role} #{parsed_attrs[:role].join ' '})
2580
2590
  end
2581
2591
 
2582
- if parsed_attrs.key? :option
2583
- (opts = parsed_attrs[:option]).each {|opt| attributes[%(#{opt}-option)] = '' }
2584
- end
2592
+ parsed_attrs[:option].each {|opt| attributes[%(#{opt}-option)] = '' } if parsed_attrs.key? :option
2585
2593
 
2586
2594
  parsed_style
2587
2595
  else
@@ -331,11 +331,12 @@ class PathResolver
331
331
 
332
332
  # Public: Securely resolve a system path
333
333
  #
334
- # Resolve a system path from the target relative to the start path, jail path, or working
335
- # directory (specified in the constructor), in that order. If a jail path is specified, enforce
336
- # that the resolved path descends from the jail path. If a jail path is not provided, the resolved
337
- # path may be any location on the system. If the resolved path is absolute, use it as is (unless
338
- # it breaches the jail path). Expand all parent and self references in the resolved path.
334
+ # Resolves the target to an absolute path on the current filesystem. The target is assumed to be
335
+ # relative to the start path, jail path, or working directory (specified in the constructor), in
336
+ # that order. If a jail path is specified, the resolved path is forced to descend from the jail
337
+ # path. If a jail path is not provided, the resolved path may be any location on the system. If
338
+ # the target is an absolute path, use it as is (unless it breaches the jail path). Expands all
339
+ # parent and self references in the resolved path.
339
340
  #
340
341
  # target - the String target path
341
342
  # start - the String start path from which to resolve a relative target; falls back to jail, if
@@ -347,8 +348,9 @@ class PathResolver
347
348
  # automatically recover when an illegal path is encountered
348
349
  # * :target_name is used in messages to refer to the path being resolved
349
350
  #
350
- # returns a String path relative to the start path, if specified, and confined to the jail path,
351
- # if specified. The path is posixified and all parent and self references in the path are expanded.
351
+ # Returns an absolute String path relative to the start path, if specified, and confined to the
352
+ # jail path, if specified. The path is posixified and all parent and self references in the path
353
+ # are expanded.
352
354
  def system_path target, start = nil, jail = nil, opts = {}
353
355
  if jail
354
356
  raise ::SecurityError, %(Jail is not an absolute path: #{jail}) unless root? jail
@@ -362,7 +364,7 @@ class PathResolver
362
364
  if jail && !(descends_from? target_path, jail)
363
365
  if opts.fetch :recover, true
364
366
  logger.warn %(#{opts[:target_name] || 'path'} is outside of jail; recovering automatically)
365
- target_segments, _ = partition_path target_path
367
+ target_segments, = partition_path target_path
366
368
  jail_segments, jail_root = partition_path jail
367
369
  return join_path jail_segments + target_segments, jail_root
368
370
  else
@@ -371,7 +373,7 @@ class PathResolver
371
373
  end
372
374
  return target_path
373
375
  else
374
- target_segments, _ = partition_path target
376
+ target_segments, = partition_path target
375
377
  end
376
378
  else
377
379
  target_segments = []
@@ -387,7 +389,7 @@ class PathResolver
387
389
  return expand_path start
388
390
  end
389
391
  else
390
- target_segments, _ = partition_path start
392
+ target_segments, = partition_path start
391
393
  start = jail || @working_dir
392
394
  end
393
395
  elsif start.nil_or_empty?
@@ -419,7 +421,7 @@ class PathResolver
419
421
  if (resolved_segments = start_segments + target_segments).include? DOT_DOT
420
422
  unresolved_segments, resolved_segments = resolved_segments, []
421
423
  if jail
422
- jail_segments, _ = partition_path jail unless jail_segments
424
+ jail_segments, = partition_path jail unless jail_segments
423
425
  warned = false
424
426
  unresolved_segments.each do |segment|
425
427
  if segment == DOT_DOT
@@ -450,7 +452,7 @@ class PathResolver
450
452
  target_path
451
453
  elsif opts.fetch :recover, true
452
454
  logger.warn %(#{opts[:target_name] || 'path'} is outside of jail; recovering automatically)
453
- jail_segments, _ = partition_path jail unless jail_segments
455
+ jail_segments, = partition_path jail unless jail_segments
454
456
  join_path jail_segments + target_segments, jail_root
455
457
  else
456
458
  raise ::SecurityError, %(#{opts[:target_name] || 'path'} #{target} is outside of jail: #{jail} (disallowed in safe mode))
@@ -44,11 +44,11 @@ class Reader
44
44
  @file = nil
45
45
  @dir = '.'
46
46
  @path = '<stdin>'
47
- @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
47
+ @lineno = 1
48
48
  elsif ::String === cursor
49
49
  @file = cursor
50
50
  @dir, @path = ::File.split @file
51
- @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
51
+ @lineno = 1
52
52
  else
53
53
  if (@file = cursor.file)
54
54
  @dir = cursor.dir || (::File.dirname @file)
@@ -57,7 +57,7 @@ class Reader
57
57
  @dir = cursor.dir || '.'
58
58
  @path = cursor.path || '<stdin>'
59
59
  end
60
- @lineno = cursor.lineno || 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
60
+ @lineno = cursor.lineno || 1
61
61
  end
62
62
  @lines = prepare_lines data, opts
63
63
  @source_lines = @lines.drop 0
@@ -570,17 +570,18 @@ class Reader
570
570
  #
571
571
  # data - A String Array or String of source data to be normalized.
572
572
  # opts - A Hash of options to control how lines are prepared.
573
- # :normalize - Enables line normalization, which coerces the encoding to UTF-8 and removes trailing whitespace
574
- # (optional, default: false).
573
+ # :normalize - Enables line normalization, which coerces the encoding to UTF-8 and removes trailing whitespace;
574
+ # :rstrip removes all trailing whitespace; :chomp removes trailing newline only (optional, not set).
575
575
  #
576
576
  # Returns A String Array of source lines. If the source data is an Array, this method returns a copy.
577
577
  def prepare_lines data, opts = {}
578
- if opts[:normalize]
579
- ::Array === data ? (Helpers.prepare_source_array data) : (Helpers.prepare_source_string data)
578
+ if (normalize = opts[:normalize])
579
+ trim_end = normalize == :chomp ? false : true
580
+ ::Array === data ? (Helpers.prepare_source_array data, trim_end) : (Helpers.prepare_source_string data, trim_end)
580
581
  elsif ::Array === data
581
582
  data.drop 0
582
583
  elsif data
583
- data.split LF, -1
584
+ data.chomp.split LF, -1
584
585
  else
585
586
  []
586
587
  end
@@ -717,7 +718,7 @@ class PreprocessorReader < Reader
717
718
  end
718
719
 
719
720
  # effectively fill the buffer
720
- if (@lines = prepare_lines data, normalize: true, condense: false, indent: attributes['indent']).empty?
721
+ if (@lines = prepare_lines data, normalize: @process_lines || :chomp, condense: false, indent: attributes['indent']).empty?
721
722
  pop_include
722
723
  else
723
724
  # FIXME we eventually want to handle leveloffset without affecting the lines
@@ -808,7 +809,6 @@ class PreprocessorReader < Reader
808
809
  end
809
810
 
810
811
  if opts.fetch :condense, true
811
- result.shift && @lineno += 1 while (first = result[0]) && first.empty?
812
812
  result.pop while (last = result[-1]) && last.empty?
813
813
  end
814
814
 
@@ -1060,7 +1060,13 @@ class PreprocessorReader < Reader
1060
1060
  return inc_path
1061
1061
  end
1062
1062
 
1063
+ if (enc = parsed_attrs['encoding']) && (::Encoding.find enc rescue nil)
1064
+ (read_mode_params = read_mode.split ':')[1] = enc
1065
+ read_mode = read_mode_params.join ':'
1066
+ end unless RUBY_ENGINE_OPAL
1067
+
1063
1068
  inc_linenos = inc_tags = nil
1069
+ # NOTE attrlist is nil if missing from include directive
1064
1070
  if attrlist
1065
1071
  if parsed_attrs.key? 'lines'
1066
1072
  inc_linenos = []
@@ -1131,9 +1137,10 @@ class PreprocessorReader < Reader
1131
1137
  else
1132
1138
  select = base_select = wildcard = inc_tags.delete '**'
1133
1139
  end
1140
+ elsif inc_tags.key? '*'
1141
+ select = base_select = !(wildcard = inc_tags.delete '*')
1134
1142
  else
1135
- select = base_select = !(inc_tags.value? true)
1136
- wildcard = inc_tags.delete '*'
1143
+ select = base_select = false
1137
1144
  end
1138
1145
  begin
1139
1146
  reader.call inc_path, read_mode do |f|
@@ -1148,7 +1155,7 @@ class PreprocessorReader < Reader
1148
1155
  active_tag, select = tag_stack.empty? ? [nil, base_select] : tag_stack[-1]
1149
1156
  elsif inc_tags.key? this_tag
1150
1157
  include_cursor = create_include_cursor inc_path, expanded_target, inc_lineno
1151
- if (idx = tag_stack.rindex {|key, _| key == this_tag })
1158
+ if (idx = tag_stack.rindex {|key,| key == this_tag })
1152
1159
  idx == 0 ? tag_stack.shift : (tag_stack.delete_at idx)
1153
1160
  logger.warn message_with_context %(mismatched end tag (expected '#{active_tag}' but found '#{this_tag}') at line #{inc_lineno} of include #{target_type}: #{inc_path}), source_location: cursor, include_location: include_cursor
1154
1161
  else
@@ -269,7 +269,7 @@ module Asciidoctor
269
269
  #
270
270
  # NOTE we only have to check as far as the blank character because we know it means non-whitespace follows.
271
271
  # IMPORTANT if this regexp does not agree with the regexp for each list type, the parser will hang.
272
- AnyListRx = %r(^(?:[ \t]*(?:-|\*\**|\.\.*|\u2022|\d+\.|[a-zA-Z]\.|[IVXivx]+\))[ \t]|(?!//[^/])[ \t]*[^ \t]#{CC_ANY}*?(?::::{0,2}|;;)(?:$|[ \t])|<?\d+>[ \t]))
272
+ AnyListRx = %r(^(?:[ \t]*(?:-|\*\**|\.\.*|\u2022|\d+\.|[a-zA-Z]\.|[IVXivx]+\))[ \t]|(?!//[^/])[ \t]*[^ \t]#{CC_ANY}*?(?::::{0,2}|;;)(?:$|[ \t])|<(?:\d+|\.)>[ \t]))
273
273
 
274
274
  # Matches an unordered list item (one level for hyphens, up to 5 levels for asterisks).
275
275
  #
@@ -469,7 +469,7 @@ module Asciidoctor
469
469
  # footnoteref:[id,text] (legacy)
470
470
  # footnoteref:[id] (legacy)
471
471
  #
472
- InlineFootnoteMacroRx = /\\?footnote(?:(ref):|:([#{CC_WORD}-]+)?)\[(?:|(#{CC_ALL}*?[^\\]))\]/m
472
+ InlineFootnoteMacroRx = /\\?footnote(?:(ref):|:([#{CC_WORD}-]+)?)\[(?:|(#{CC_ALL}*?[^\\]))\](?!<\/a>)/m
473
473
 
474
474
  # Matches an image or icon inline macro.
475
475
  #
@@ -514,9 +514,10 @@ module Asciidoctor
514
514
  # https://github.com[GitHub]
515
515
  # <https://github.com>
516
516
  # link:https://github.com[]
517
+ # "https://github.com[]"
517
518
  #
518
519
  # FIXME revisit! the main issue is we need different rules for implicit vs explicit
519
- InlineLinkRx = %r((^|link:|#{CG_BLANK}|&lt;|[>\(\)\[\];])(\\?(?:https?|file|ftp|irc)://[^\s\[\]<]*([^\s.,\[\]<]))(?:\[(|#{CC_ALL}*?[^\\])\])?)m
520
+ InlineLinkRx = %r((^|link:|#{CG_BLANK}|&lt;|[>\(\)\[\];"'])(\\?(?:https?|file|ftp|irc)://[^\s\[\]<]*([^\s.,\[\]<]))(?:\[(|#{CC_ALL}*?[^\\])\])?)m
520
521
 
521
522
  # Match a link or e-mail inline macro.
522
523
  #
@@ -551,7 +552,7 @@ module Asciidoctor
551
552
  # menu:View[Page Style > No Style]
552
553
  # menu:View[Page Style, No Style]
553
554
  #
554
- InlineMenuMacroRx = /\\?menu:(#{CG_WORD}|[#{CC_WORD}&][^\n\[]*[^\s\[])\[ *(?:|(#{CC_ALL}*?[^\\]))?\]/m
555
+ InlineMenuMacroRx = /\\?menu:(#{CG_WORD}|[#{CC_WORD}&][^\n\[]*[^\s\[])\[ *(?:|(#{CC_ALL}*?[^\\]))\]/m
555
556
 
556
557
  # Matches an implicit menu inline macro.
557
558
  #
@@ -591,7 +592,7 @@ module Asciidoctor
591
592
  # $$text$$
592
593
  # pass:quotes[text]
593
594
  #
594
- # NOTE we have to support an empty pass:[] for compatibility with AsciiDoc Python
595
+ # NOTE we have to support an empty pass:[] for compatibility with AsciiDoc.py
595
596
  InlinePassMacroRx = /(?:(?:(\\?)\[([^\]]+)\])?(\\{0,2})(\+\+\+?|\$\$)(#{CC_ALL}*?)\4|(\\?)pass:([a-z]+(?:,[a-z-]+)*)?\[(|#{CC_ALL}*?[^\\])\])/m
596
597
 
597
598
  # Matches an xref (i.e., cross-reference) inline macro, which may span multiple lines.
@@ -610,7 +611,7 @@ module Asciidoctor
610
611
  # Matches a trailing + preceded by at least one space character,
611
612
  # which forces a hard line break (<br> tag in HTML output).
612
613
  #
613
- # NOTE AsciiDoc Python allows + to be preceded by TAB; Asciidoctor does not
614
+ # NOTE AsciiDoc.py allows + to be preceded by TAB; Asciidoctor does not
614
615
  #
615
616
  # Examples
616
617
  #
@@ -331,8 +331,8 @@ module Substitutors
331
331
  target ||= ext_config[:format] == :short ? content : target
332
332
  end
333
333
  if (Inline === (replacement = extension.process_method[self, target, attributes]))
334
- if (inline_subs = replacement.attributes.delete 'subs')
335
- replacement.text = apply_subs replacement.text, (expand_subs inline_subs)
334
+ if (inline_subs = replacement.attributes.delete 'subs') && (inline_subs = expand_subs inline_subs, 'custom inline macro')
335
+ replacement.text = apply_subs replacement.text, inline_subs
336
336
  end
337
337
  replacement.convert
338
338
  elsif replacement
@@ -542,14 +542,17 @@ module Substitutors
542
542
  end
543
543
 
544
544
  prefix, suffix = $1, ''
545
- # NOTE if $4 is set, then we're looking at a formal macro
545
+ # NOTE if $4 is set, we're looking at a formal macro (e.g., https://example.org[])
546
546
  if $4
547
547
  prefix = '' if prefix == 'link:'
548
548
  text = $4
549
549
  else
550
- # invalid macro syntax (link: prefix w/o trailing square brackets)
551
- # FIXME we probably shouldn't even get here...our regex is doing too much
552
- next $& if prefix == 'link:'
550
+ # invalid macro syntax (link: prefix w/o trailing square brackets or enclosed in double quotes)
551
+ # FIXME we probably shouldn't even get here when the link: prefix is present; the regex is doing too much
552
+ case prefix
553
+ when 'link:', ?", ?'
554
+ next $&
555
+ end
553
556
  text = ''
554
557
  case $3
555
558
  when ')'
@@ -591,7 +594,8 @@ module Substitutors
591
594
  unless text.empty?
592
595
  text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
593
596
  if !doc.compat_mode && (text.include? '=')
594
- text = (attrs = (AttributeList.new text, self).parse)[1] || ''
597
+ # NOTE if an equals sign (=) is present, extract attributes from text
598
+ text, attrs = extract_attributes_from_text text, ''
595
599
  link_opts[:id] = attrs['id']
596
600
  end
597
601
 
@@ -637,7 +641,8 @@ module Substitutors
637
641
  text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
638
642
  if mailto
639
643
  if !doc.compat_mode && (text.include? ',')
640
- text = (attrs = (AttributeList.new text, self).parse)[1] || ''
644
+ # NOTE if a comma (,) is present, extract attributes from text
645
+ text, attrs = extract_attributes_from_text text, ''
641
646
  link_opts[:id] = attrs['id']
642
647
  if attrs.key? 2
643
648
  if attrs.key? 3
@@ -648,7 +653,8 @@ module Substitutors
648
653
  end
649
654
  end
650
655
  elsif !doc.compat_mode && (text.include? '=')
651
- text = (attrs = (AttributeList.new text, self).parse)[1] || ''
656
+ # NOTE if an equals sign (=) is present, extract attributes from text
657
+ text, attrs = extract_attributes_from_text text, ''
652
658
  link_opts[:id] = attrs['id']
653
659
  end
654
660
 
@@ -739,8 +745,8 @@ module Substitutors
739
745
  refid = $2
740
746
  if (text = $3)
741
747
  text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
742
- # NOTE if an equal sign (=) is present, parse text as attributes
743
- text = ((AttributeList.new text, self).parse_into attrs)[1] if !doc.compat_mode && (text.include? '=')
748
+ # NOTE if an equals sign (=) is present, extract attributes from text
749
+ text, attrs = extract_attributes_from_text text if !doc.compat_mode && (text.include? '=')
744
750
  end
745
751
  end
746
752
 
@@ -804,7 +810,7 @@ module Substitutors
804
810
  # handles: id (in compat mode or when natural xrefs are disabled)
805
811
  elsif doc.compat_mode || !Compliance.natural_xrefs
806
812
  refid, target = fragment, %(##{fragment})
807
- logger.info %(possible invalid reference: #{refid}) if logger.info? && doc.catalog[:refs][refid]
813
+ logger.info %(possible invalid reference: #{refid}) if logger.info? && !doc.catalog[:refs][refid]
808
814
  # handles: id
809
815
  elsif doc.catalog[:refs][fragment]
810
816
  refid, target = fragment, %(##{fragment})
@@ -843,19 +849,17 @@ module Substitutors
843
849
  end
844
850
 
845
851
  if id
846
- if text
852
+ if (footnote = doc.footnotes.find {|candidate| candidate.id == id })
853
+ index, text = footnote.index, footnote.text
854
+ type, target, id = :xref, id, nil
855
+ elsif text
847
856
  text = restore_passthroughs(normalize_text text, true, true)
848
857
  index = doc.counter('footnote-number')
849
858
  doc.register(:footnotes, Document::Footnote.new(index, id, text))
850
859
  type, target = :ref, nil
851
860
  else
852
- if (footnote = doc.footnotes.find {|candidate| candidate.id == id })
853
- index, text = footnote.index, footnote.text
854
- else
855
- logger.warn %(invalid footnote reference: #{id})
856
- index, text = nil, id
857
- end
858
- type, target, id = :xref, id, nil
861
+ logger.warn %(invalid footnote reference: #{id})
862
+ type, target, text, id = :xref, id, id, nil
859
863
  end
860
864
  elsif text
861
865
  text = restore_passthroughs(normalize_text text, true, true)
@@ -917,7 +921,7 @@ module Substitutors
917
921
  # use sub since it might be behind a line comment
918
922
  $&.sub RS, ''
919
923
  else
920
- Inline.new(self, :callout, $4 == '.' ? (autonum += 1).to_s : $4, id: @document.callouts.read_next_id, attributes: { 'guard' => $1 }).convert
924
+ Inline.new(self, :callout, $4 == '.' ? (autonum += 1).to_s : $4, id: @document.callouts.read_next_id, attributes: { 'guard' => $1 || ($3 == '--' ? ['<!--', '-->'] : nil) }).convert
921
925
  end
922
926
  end
923
927
  end
@@ -945,7 +949,7 @@ module Substitutors
945
949
  if (linenums_mode = (attr? 'linenums') ? (doc_attrs[%(#{syntax_hl_name}-linenums-mode)] || :table).to_sym : nil)
946
950
  start_line_number = 1 if (start_line_number = (attr 'start', 1).to_i) < 1
947
951
  end
948
- highlight_lines = resolve_lines_to_highlight source, (attr 'highlight') if attr? 'highlight'
952
+ highlight_lines = resolve_lines_to_highlight source, (attr 'highlight'), start_line_number if attr? 'highlight'
949
953
 
950
954
  highlighted, source_offset = syntax_hl.highlight self, source, (attr 'language'),
951
955
  callouts: callout_marks,
@@ -968,9 +972,10 @@ module Substitutors
968
972
  #
969
973
  # source - The String source.
970
974
  # spec - The lines specifier (e.g., "1-5, !2, 10" or "1..5;!2;10")
975
+ # start - The line number of the first line (optional, default: false)
971
976
  #
972
977
  # Returns an [Array] of unique, sorted line numbers.
973
- def resolve_lines_to_highlight source, spec
978
+ def resolve_lines_to_highlight source, spec, start = nil
974
979
  lines = []
975
980
  spec = spec.delete ' ' if spec.include? ' '
976
981
  ((spec.include? ',') ? (spec.split ',') : (spec.split ';')).map do |entry|
@@ -981,21 +986,22 @@ module Substitutors
981
986
  if (delim = (entry.include? '..') ? '..' : ((entry.include? '-') ? '-' : nil))
982
987
  from, delim, to = entry.partition delim
983
988
  to = (source.count LF) + 1 if to.empty? || (to = to.to_i) < 0
984
- line_nums = (from.to_i..to).to_a
985
- if negate
986
- lines -= line_nums
987
- else
988
- lines.concat line_nums
989
- end
990
- else
991
989
  if negate
992
- lines.delete entry.to_i
990
+ lines -= (from.to_i..to).to_a
993
991
  else
994
- lines << entry.to_i
992
+ lines |= (from.to_i..to).to_a
995
993
  end
994
+ elsif negate
995
+ lines.delete entry.to_i
996
+ elsif !lines.include?(line = entry.to_i)
997
+ lines << line
996
998
  end
997
999
  end
998
- lines.sort.uniq
1000
+ # If the start attribute is defined, then the lines to highlight specified by the provided spec should be relative to the start value.
1001
+ unless (shift = start ? start - 1 : 0) == 0
1002
+ lines = lines.map {|it| it - shift }
1003
+ end
1004
+ lines.sort
999
1005
  end
1000
1006
 
1001
1007
  # Public: Extract the passthrough text from the document for reinsertion after processing.
@@ -1112,7 +1118,7 @@ module Substitutors
1112
1118
  end
1113
1119
  subs = $2
1114
1120
  content = normalize_text $3, nil, true
1115
- # NOTE drop enclosing $ signs around latexmath for backwards compatibility with AsciiDoc Python
1121
+ # NOTE drop enclosing $ signs around latexmath for backwards compatibility with AsciiDoc.py
1116
1122
  content = content.slice 1, content.length - 2 if type == :latexmath && (content.start_with? '$') && (content.end_with? '$')
1117
1123
  subs = subs ? (resolve_pass_subs subs) : ((@document.basebackend? 'html') ? BASIC_SUBS : nil)
1118
1124
  passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, type: type }
@@ -1226,17 +1232,16 @@ module Substitutors
1226
1232
  resolve_subs subs, :inline, nil, 'passthrough macro'
1227
1233
  end
1228
1234
 
1229
- # Public: Expand all groups in the subs list and return. If no subs are resolve, return nil.
1235
+ # Public: Expand all groups in the subs list and return. If no subs are resolved, return nil.
1230
1236
  #
1231
- # subs - The substitutions to expand; can be a Symbol, Symbol Array or nil
1237
+ # subs - The substitutions to expand; can be a Symbol, Symbol Array, or String
1238
+ # subject - The String to use in log messages to communicate the subject for which subs are being resolved (default: nil)
1232
1239
  #
1233
1240
  # Returns a Symbol Array of substitutions to pass to apply_subs or nil if no substitutions were resolved.
1234
- def expand_subs subs
1241
+ def expand_subs subs, subject = nil
1235
1242
  if ::Symbol === subs
1236
- unless subs == :none
1237
- SUB_GROUPS[subs] || [subs]
1238
- end
1239
- else
1243
+ subs == :none ? nil : SUB_GROUPS[subs] || [subs]
1244
+ elsif ::Array === subs
1240
1245
  expanded_subs = []
1241
1246
  subs.each do |key|
1242
1247
  unless key == :none
@@ -1247,8 +1252,9 @@ module Substitutors
1247
1252
  end
1248
1253
  end
1249
1254
  end
1250
-
1251
1255
  expanded_subs.empty? ? nil : expanded_subs
1256
+ else
1257
+ resolve_subs subs, :inline, nil, subject
1252
1258
  end
1253
1259
  end
1254
1260
 
@@ -1270,7 +1276,7 @@ module Substitutors
1270
1276
  # NOTE :literal with listparagraph-option gets folded into text of list item later
1271
1277
  default_subs = @context == :verse ? NORMAL_SUBS : VERBATIM_SUBS
1272
1278
  when :raw
1273
- # TODO make pass subs a compliance setting; AsciiDoc Python performs :attributes and :macros on a pass block
1279
+ # TODO make pass subs a compliance setting; AsciiDoc.py performs :attributes and :macros on a pass block
1274
1280
  default_subs = @context == :stem ? BASIC_SUBS : NO_SUBS
1275
1281
  else
1276
1282
  return @subs
@@ -1321,10 +1327,23 @@ module Substitutors
1321
1327
 
1322
1328
  private
1323
1329
 
1330
+ # This method is used in cases when the attrlist can be mixed with the text of a macro.
1331
+ # If no attributes are detected aside from the first positional attribute, and the first positional
1332
+ # attribute matches the attrlist, then the original text is returned.
1333
+ def extract_attributes_from_text text, default_text = nil
1334
+ attrlist = (text.include? LF) ? (text.tr LF, ' ') : text
1335
+ if (resolved_text = (attrs = (AttributeList.new attrlist, self).parse)[1])
1336
+ # NOTE if resolved text remains unchanged, clear attributes and return unparsed text
1337
+ resolved_text == attrlist ? [text, attrs.clear] : [resolved_text, attrs]
1338
+ else
1339
+ [default_text, attrs]
1340
+ end
1341
+ end
1342
+
1324
1343
  # Internal: Extract the callout numbers from the source to prepare it for syntax highlighting.
1325
1344
  def extract_callouts source
1326
1345
  callout_marks = {}
1327
- lineno = 0
1346
+ autonum = lineno = 0
1328
1347
  last_lineno = nil
1329
1348
  callout_rx = (attr? 'line-comment') ? CalloutExtractRxMap[attr 'line-comment'] : CalloutExtractRx
1330
1349
  # extract callout marks, indexed by line number
@@ -1336,7 +1355,7 @@ module Substitutors
1336
1355
  # use sub since it might be behind a line comment
1337
1356
  $&.sub RS, ''
1338
1357
  else
1339
- (callout_marks[lineno] ||= []) << [$1, $4]
1358
+ (callout_marks[lineno] ||= []) << [$1 || ($3 == '--' ? ['<!--', '-->'] : nil), $4 == '.' ? (autonum += 1).to_s : $4]
1340
1359
  last_lineno = lineno
1341
1360
  ''
1342
1361
  end
@@ -1358,15 +1377,15 @@ module Substitutors
1358
1377
  else
1359
1378
  preamble = ''
1360
1379
  end
1361
- autonum = lineno = 0
1380
+ lineno = 0
1362
1381
  preamble + ((source.split LF, -1).map do |line|
1363
1382
  if (conums = callout_marks.delete lineno += 1)
1364
1383
  if conums.size == 1
1365
- guard, conum = conums[0]
1366
- %(#{line}#{Inline.new(self, :callout, conum == '.' ? (autonum += 1).to_s : conum, id: @document.callouts.read_next_id, attributes: { 'guard' => guard }).convert})
1384
+ guard, numeral = conums[0]
1385
+ %(#{line}#{Inline.new(self, :callout, numeral, id: @document.callouts.read_next_id, attributes: { 'guard' => guard }).convert})
1367
1386
  else
1368
- %(#{line}#{conums.map do |guard_it, conum_it|
1369
- Inline.new(self, :callout, conum_it == '.' ? (autonum += 1).to_s : conum_it, id: @document.callouts.read_next_id, attributes: { 'guard' => guard_it }).convert
1387
+ %(#{line}#{conums.map do |guard_it, numeral_it|
1388
+ Inline.new(self, :callout, numeral_it, id: @document.callouts.read_next_id, attributes: { 'guard' => guard_it }).convert
1370
1389
  end.join ' '})
1371
1390
  end
1372
1391
  else