asciidoctor 2.0.12 → 2.0.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +142 -22
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +15 -6
  5. data/README-fr.adoc +14 -8
  6. data/README-jp.adoc +15 -6
  7. data/README-zh_CN.adoc +14 -5
  8. data/README.adoc +135 -125
  9. data/asciidoctor.gemspec +4 -11
  10. data/data/locale/attributes-be.adoc +23 -0
  11. data/data/locale/attributes-it.adoc +4 -4
  12. data/data/locale/attributes-nl.adoc +6 -6
  13. data/data/locale/attributes-th.adoc +23 -0
  14. data/data/locale/attributes-vi.adoc +23 -0
  15. data/data/reference/syntax.adoc +14 -7
  16. data/data/stylesheets/asciidoctor-default.css +51 -52
  17. data/lib/asciidoctor.rb +12 -12
  18. data/lib/asciidoctor/abstract_block.rb +4 -4
  19. data/lib/asciidoctor/abstract_node.rb +10 -9
  20. data/lib/asciidoctor/attribute_list.rb +6 -6
  21. data/lib/asciidoctor/block.rb +6 -6
  22. data/lib/asciidoctor/cli/invoker.rb +0 -1
  23. data/lib/asciidoctor/cli/options.rb +27 -26
  24. data/lib/asciidoctor/convert.rb +1 -0
  25. data/lib/asciidoctor/converter.rb +5 -3
  26. data/lib/asciidoctor/converter/docbook5.rb +45 -26
  27. data/lib/asciidoctor/converter/html5.rb +89 -69
  28. data/lib/asciidoctor/converter/manpage.rb +113 -86
  29. data/lib/asciidoctor/converter/template.rb +11 -12
  30. data/lib/asciidoctor/document.rb +44 -51
  31. data/lib/asciidoctor/extensions.rb +10 -12
  32. data/lib/asciidoctor/helpers.rb +3 -6
  33. data/lib/asciidoctor/list.rb +2 -6
  34. data/lib/asciidoctor/load.rb +13 -11
  35. data/lib/asciidoctor/logging.rb +10 -8
  36. data/lib/asciidoctor/parser.rb +135 -150
  37. data/lib/asciidoctor/path_resolver.rb +3 -3
  38. data/lib/asciidoctor/reader.rb +72 -71
  39. data/lib/asciidoctor/rx.rb +4 -3
  40. data/lib/asciidoctor/substitutors.rb +117 -117
  41. data/lib/asciidoctor/syntax_highlighter.rb +8 -11
  42. data/lib/asciidoctor/syntax_highlighter/coderay.rb +2 -1
  43. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +1 -1
  44. data/lib/asciidoctor/syntax_highlighter/pygments.rb +6 -5
  45. data/lib/asciidoctor/syntax_highlighter/rouge.rb +33 -26
  46. data/lib/asciidoctor/table.rb +17 -19
  47. data/lib/asciidoctor/timings.rb +3 -3
  48. data/lib/asciidoctor/version.rb +1 -1
  49. data/man/asciidoctor.1 +10 -11
  50. data/man/asciidoctor.adoc +8 -7
  51. metadata +14 -67
@@ -109,7 +109,7 @@ class PathResolver
109
109
  SLASH = '/'
110
110
  BACKSLASH = '\\'
111
111
  DOUBLE_SLASH = '//'
112
- WindowsRootRx = /^(?:[a-zA-Z]:)?[\\\/]/
112
+ WindowsRootRx = %r(^(?:[a-zA-Z]:)?[\\/])
113
113
 
114
114
  attr_accessor :file_separator
115
115
  attr_accessor :working_dir
@@ -290,8 +290,8 @@ class PathResolver
290
290
  # ex. ./sample/path
291
291
  elsif posix_path.start_with? DOT_SLASH
292
292
  root = DOT_SLASH
293
- # else ex. sample/path
294
293
  end
294
+ # otherwise ex. sample/path
295
295
  elsif root? posix_path
296
296
  # ex. //sample/path
297
297
  if unc? posix_path
@@ -306,8 +306,8 @@ class PathResolver
306
306
  # ex. ./sample/path
307
307
  elsif posix_path.start_with? DOT_SLASH
308
308
  root = DOT_SLASH
309
- # else ex. sample/path
310
309
  end
310
+ # otherwise ex. sample/path
311
311
 
312
312
  path_segments = (root ? (posix_path.slice root.length, posix_path.length) : posix_path).split SLASH
313
313
  # strip out all dot entries
@@ -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,10 +57,9 @@ 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
- @lines = prepare_lines data, opts
63
- @source_lines = @lines.drop 0
62
+ @lines = (@source_lines = prepare_lines data, opts).reverse
64
63
  @mark = nil
65
64
  @look_ahead = 0
66
65
  @process_lines = true
@@ -127,7 +126,7 @@ class Reader
127
126
  # Returns nothing if there is no more data.
128
127
  def peek_line direct = false
129
128
  if direct || @look_ahead > 0
130
- @unescape_next_line ? ((line = @lines[0]).slice 1, line.length) : @lines[0]
129
+ @unescape_next_line ? ((line = @lines[-1]).slice 1, line.length) : @lines[-1]
131
130
  elsif @lines.empty?
132
131
  @look_ahead = 0
133
132
  nil
@@ -135,7 +134,7 @@ class Reader
135
134
  # FIXME the problem with this approach is that we aren't
136
135
  # retaining the modified line (hence the @unescape_next_line tweak)
137
136
  # perhaps we need a stack of proxied lines
138
- (line = process_line @lines[0]) ? line : peek_line
137
+ (process_line @lines[-1]) || peek_line
139
138
  end
140
139
  end
141
140
 
@@ -192,9 +191,7 @@ class Reader
192
191
  def read_lines
193
192
  lines = []
194
193
  # has_more_lines? triggers preprocessor
195
- while has_more_lines?
196
- lines << shift
197
- end
194
+ lines << shift while has_more_lines?
198
195
  lines
199
196
  end
200
197
  alias readlines read_lines
@@ -241,7 +238,6 @@ class Reader
241
238
  # Returns nothing.
242
239
  def unshift_lines lines_to_restore
243
240
  unshift_all lines_to_restore
244
- nil
245
241
  end
246
242
  alias restore_lines unshift_lines
247
243
 
@@ -334,7 +330,7 @@ class Reader
334
330
  comment_lines = []
335
331
  # optimized code for shortest execution path
336
332
  while (next_line = peek_line) && !next_line.empty?
337
- if (next_line.start_with? '//')
333
+ if next_line.start_with? '//'
338
334
  comment_lines << shift
339
335
  else
340
336
  break
@@ -407,34 +403,22 @@ class Reader
407
403
  break_on_list_continuation = options[:break_on_list_continuation]
408
404
  end
409
405
  skip_comments = options[:skip_line_comments]
410
- complete = line_read = line_restored = nil
406
+ line_read = line_restored = nil
411
407
  shift if options[:skip_first_line]
412
- while !complete && (line = read_line)
413
- complete = while true
414
- break true if terminator && line == terminator
415
- # QUESTION: can we get away with line.empty? here?
416
- break true if break_on_blank_lines && line.empty?
417
- if break_on_list_continuation && line_read && line == LIST_CONTINUATION
418
- options[:preserve_last_line] = true
419
- break true
420
- end
421
- break true if block_given? && (yield line)
422
- break false
423
- end
424
- if complete
425
- if options[:read_last_line]
426
- result << line
427
- line_read = true
428
- end
408
+ while (line = read_line)
409
+ if terminator ? line == terminator : ((break_on_blank_lines && line.empty?) ||
410
+ (break_on_list_continuation && line_read && line == LIST_CONTINUATION && (options[:preserve_last_line] = true)) ||
411
+ (block_given? && (yield line)))
412
+ result << line if options[:read_last_line]
429
413
  if options[:preserve_last_line]
430
414
  unshift line
431
415
  line_restored = true
432
416
  end
433
- else
434
- unless skip_comments && (line.start_with? '//') && !(line.start_with? '///')
435
- result << line
436
- line_read = true
437
- end
417
+ break
418
+ end
419
+ unless skip_comments && (line.start_with? '//') && !(line.start_with? '///')
420
+ result << line
421
+ line_read = true
438
422
  end
439
423
  end
440
424
  if restore_process_lines
@@ -459,21 +443,37 @@ class Reader
459
443
  def shift
460
444
  @lineno += 1
461
445
  @look_ahead -= 1 unless @look_ahead == 0
462
- @lines.shift
446
+ @lines.pop
463
447
  end
464
448
 
465
449
  # Internal: Restore the line to the stack and decrement the lineno
466
450
  def unshift line
467
451
  @lineno -= 1
468
452
  @look_ahead += 1
469
- @lines.unshift line
453
+ @lines.push line
454
+ nil
470
455
  end
471
456
 
472
- # Internal: Restore the lines to the stack and decrement the lineno
473
- def unshift_all lines
474
- @lineno -= lines.size
475
- @look_ahead += lines.size
476
- @lines.unshift(*lines)
457
+ if ::RUBY_ENGINE == 'jruby'
458
+ # Internal: Restore the lines to the stack and decrement the lineno
459
+ def unshift_all lines_to_restore
460
+ @lineno -= lines_to_restore.size
461
+ @look_ahead += lines_to_restore.size
462
+ if lines_to_restore.respond_to? :reverse
463
+ @lines.push(*lines_to_restore.reverse)
464
+ else
465
+ lines_to_restore.reverse_each {|it| @lines.push it }
466
+ end
467
+ nil
468
+ end
469
+ else
470
+ # Internal: Restore the lines to the stack and decrement the lineno
471
+ def unshift_all lines_to_restore
472
+ @lineno -= lines_to_restore.size
473
+ @look_ahead += lines_to_restore.size
474
+ @lines.push(*lines_to_restore.reverse)
475
+ nil
476
+ end
477
477
  end
478
478
 
479
479
  def cursor
@@ -516,12 +516,12 @@ class Reader
516
516
  #
517
517
  # Returns A copy of the String Array of lines remaining in this Reader
518
518
  def lines
519
- @lines.drop 0
519
+ @lines.reverse
520
520
  end
521
521
 
522
522
  # Public: Get a copy of the remaining lines managed by this Reader joined as a String
523
523
  def string
524
- @lines.join LF
524
+ @lines.reverse.join LF
525
525
  end
526
526
 
527
527
  # Public: Get the source lines for this Reader joined as a String
@@ -576,8 +576,7 @@ class Reader
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
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)
579
+ ::Array === data ? (Helpers.prepare_source_array data, normalize != :chomp) : (Helpers.prepare_source_string data, normalize != :chomp)
581
580
  elsif ::Array === data
582
581
  data.drop 0
583
582
  elsif data
@@ -690,6 +689,7 @@ class PreprocessorReader < Reader
690
689
  @path = (path ||= ::File.basename file)
691
690
  # only process lines in AsciiDoc files
692
691
  if (@process_lines = file.end_with?(*ASCIIDOC_EXTENSIONS.keys))
692
+ # NOTE registering the include with a nil value tracks it while not making it visible to interdocument xrefs
693
693
  @includes[path.slice 0, (path.rindex '.')] = attributes['partial-option'] ? nil : true
694
694
  end
695
695
  else
@@ -697,6 +697,7 @@ class PreprocessorReader < Reader
697
697
  # we don't know what file type we have, so assume AsciiDoc
698
698
  @process_lines = true
699
699
  if (@path = path)
700
+ # NOTE registering the include with a nil value tracks it while not making it visible to interdocument xrefs
700
701
  @includes[Helpers.rootname path] = attributes['partial-option'] ? nil : true
701
702
  else
702
703
  @path = '<stdin>'
@@ -718,21 +719,16 @@ class PreprocessorReader < Reader
718
719
  end
719
720
 
720
721
  # effectively fill the buffer
721
- if (@lines = prepare_lines data, normalize: @process_lines || :chomp, condense: @process_lines, indent: attributes['indent']).empty?
722
+ if (@lines = prepare_lines data, normalize: @process_lines || :chomp, condense: false, indent: attributes['indent']).empty?
722
723
  pop_include
723
724
  else
724
725
  # FIXME we eventually want to handle leveloffset without affecting the lines
725
726
  if attributes.key? 'leveloffset'
726
- @lines.unshift ''
727
- @lines.unshift %(:leveloffset: #{attributes['leveloffset']})
728
- @lines << ''
729
- if (old_leveloffset = @document.attr 'leveloffset')
730
- @lines << %(:leveloffset: #{old_leveloffset})
731
- else
732
- @lines << ':leveloffset!:'
733
- end
734
- # compensate for these extra lines
727
+ @lines = [((leveloffset = @document.attr 'leveloffset') ? %(:leveloffset: #{leveloffset}) : ':leveloffset!:'), ''] + @lines.reverse + ['', %(:leveloffset: #{attributes['leveloffset']})]
728
+ # compensate for these extra lines at the top
735
729
  @lineno -= 2
730
+ else
731
+ @lines.reverse!
736
732
  end
737
733
 
738
734
  # FIXME kind of a hack
@@ -802,14 +798,11 @@ class PreprocessorReader < Reader
802
798
  result = super
803
799
 
804
800
  # QUESTION should this work for AsciiDoc table cell content? Currently it does not.
805
- if @document && @document.attributes['skip-front-matter']
806
- if (front_matter = skip_front_matter! result)
807
- @document.attributes['front-matter'] = front_matter.join LF
808
- end
801
+ if @document && @document.attributes['skip-front-matter'] && (front_matter = skip_front_matter! result)
802
+ @document.attributes['front-matter'] = front_matter.join LF
809
803
  end
810
804
 
811
805
  if opts.fetch :condense, true
812
- result.shift && @lineno += 1 while (first = result[0]) && first.empty?
813
806
  result.pop while (last = result[-1]) && last.empty?
814
807
  end
815
808
 
@@ -956,11 +949,12 @@ class PreprocessorReader < Reader
956
949
  if no_target
957
950
  # the text in brackets must match a conditional expression
958
951
  if text && EvalExpressionRx =~ text.strip
952
+ # NOTE assignments must happen before call to resolve_expr_val for compatiblity with Opal
959
953
  lhs = $1
954
+ # regex enforces a restricted set of math-related operations (==, !=, <=, >=, <, >)
960
955
  op = $2
961
956
  rhs = $3
962
- # regex enforces a restricted set of math-related operations (==, !=, <=, >=, <, >)
963
- skip = ((resolve_expr_val lhs).send op, (resolve_expr_val rhs)) ? false : true
957
+ skip = ((resolve_expr_val lhs).send op, (resolve_expr_val rhs)) ? false : true rescue true
964
958
  else
965
959
  logger.error message_with_context %(malformed preprocessor directive - #{text ? 'invalid expression' : 'missing expression'}: ifeval::[#{text}]), source_location: cursor
966
960
  return true
@@ -1050,10 +1044,11 @@ class PreprocessorReader < Reader
1050
1044
 
1051
1045
  parsed_attrs = doc.parse_attributes attrlist, [], sub_input: true
1052
1046
  inc_path, target_type, relpath = resolve_include_path expanded_target, attrlist, parsed_attrs
1053
- if target_type == :file
1047
+ case target_type
1048
+ when :file
1054
1049
  reader = ::File.method :open
1055
1050
  read_mode = FILE_READ_MODE
1056
- elsif target_type == :uri
1051
+ when :uri
1057
1052
  reader = ::OpenURI.method :open_uri
1058
1053
  read_mode = URI_READ_MODE
1059
1054
  else
@@ -1074,7 +1069,7 @@ class PreprocessorReader < Reader
1074
1069
  (split_delimited_value parsed_attrs['lines']).each do |linedef|
1075
1070
  if linedef.include? '..'
1076
1071
  from, _, to = linedef.partition '..'
1077
- inc_linenos += (to.empty? || (to = to.to_i) < 0) ? [from.to_i, 1.0/0.0] : (from.to_i..to).to_a
1072
+ inc_linenos += (to.empty? || (to = to.to_i) < 0) ? [from.to_i, ::Float::INFINITY] : (from.to_i..to).to_a
1078
1073
  else
1079
1074
  inc_linenos << linedef.to_i
1080
1075
  end
@@ -1132,15 +1127,21 @@ class PreprocessorReader < Reader
1132
1127
  elsif inc_tags
1133
1128
  inc_lines, inc_offset, inc_lineno, tag_stack, tags_used, active_tag = [], nil, 0, [], ::Set.new, nil
1134
1129
  if inc_tags.key? '**'
1130
+ select = base_select = inc_tags.delete '**'
1135
1131
  if inc_tags.key? '*'
1136
- select = base_select = inc_tags.delete '**'
1137
1132
  wildcard = inc_tags.delete '*'
1133
+ elsif !select && inc_tags.values.first == false
1134
+ wildcard = true
1135
+ end
1136
+ elsif inc_tags.key? '*'
1137
+ if inc_tags.keys.first == '*'
1138
+ select = base_select = !(wildcard = inc_tags.delete '*')
1138
1139
  else
1139
- select = base_select = wildcard = inc_tags.delete '**'
1140
+ select = base_select = false
1141
+ wildcard = inc_tags.delete '*'
1140
1142
  end
1141
1143
  else
1142
1144
  select = base_select = !(inc_tags.value? true)
1143
- wildcard = inc_tags.delete '*'
1144
1145
  end
1145
1146
  begin
1146
1147
  reader.call inc_path, read_mode do |f|
@@ -1191,7 +1192,7 @@ class PreprocessorReader < Reader
1191
1192
  end
1192
1193
  shift
1193
1194
  if inc_offset
1194
- parsed_attrs['partial-option'] = '' unless base_select && wildcard && inc_tags.empty?
1195
+ parsed_attrs['partial-option'] = '' unless base_select && wildcard != false && inc_tags.empty?
1195
1196
  # FIXME not accounting for skipped lines in reader line numbering
1196
1197
  push_include inc_lines, inc_path, relpath, inc_offset, parsed_attrs
1197
1198
  end
@@ -1262,7 +1263,7 @@ class PreprocessorReader < Reader
1262
1263
  end
1263
1264
 
1264
1265
  def pop_include
1265
- if @include_stack.size > 0
1266
+ unless @include_stack.empty?
1266
1267
  @lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines = @include_stack.pop
1267
1268
  # FIXME kind of a hack
1268
1269
  #Document::AttributeEntry.new('infile', @file).save_to_next_block @document
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Asciidoctor
2
3
  # A collection of regular expression constants used by the parser. (For speed, these are not defined in the Rx module,
3
4
  # but rather directly in the Asciidoctor module).
@@ -469,7 +470,7 @@ module Asciidoctor
469
470
  # footnoteref:[id,text] (legacy)
470
471
  # footnoteref:[id] (legacy)
471
472
  #
472
- InlineFootnoteMacroRx = /\\?footnote(?:(ref):|:([#{CC_WORD}-]+)?)\[(?:|(#{CC_ALL}*?[^\\]))\](?!<\/a>)/m
473
+ InlineFootnoteMacroRx = %r(\\?footnote(?:(ref):|:([#{CC_WORD}-]+)?)\[(?:|(#{CC_ALL}*?[^\\]))\](?!</a>))m
473
474
 
474
475
  # Matches an image or icon inline macro.
475
476
  #
@@ -592,7 +593,7 @@ module Asciidoctor
592
593
  # $$text$$
593
594
  # pass:quotes[text]
594
595
  #
595
- # NOTE we have to support an empty pass:[] for compatibility with AsciiDoc Python
596
+ # NOTE we have to support an empty pass:[] for compatibility with AsciiDoc.py
596
597
  InlinePassMacroRx = /(?:(?:(\\?)\[([^\]]+)\])?(\\{0,2})(\+\+\+?|\$\$)(#{CC_ALL}*?)\4|(\\?)pass:([a-z]+(?:,[a-z-]+)*)?\[(|#{CC_ALL}*?[^\\])\])/m
597
598
 
598
599
  # Matches an xref (i.e., cross-reference) inline macro, which may span multiple lines.
@@ -611,7 +612,7 @@ module Asciidoctor
611
612
  # Matches a trailing + preceded by at least one space character,
612
613
  # which forces a hard line break (<br> tag in HTML output).
613
614
  #
614
- # NOTE AsciiDoc Python allows + to be preceded by TAB; Asciidoctor does not
615
+ # NOTE AsciiDoc.py allows + to be preceded by TAB; Asciidoctor does not
615
616
  #
616
617
  # Examples
617
618
  #
@@ -330,13 +330,13 @@ module Substitutors
330
330
  # NOTE for convenience, map content (unparsed attrlist) to target when format is short
331
331
  target ||= ext_config[:format] == :short ? content : target
332
332
  end
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)
333
+ if Inline === (replacement = extension.process_method[self, target, attributes])
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
339
- logger.info %(expected substitution value for custom inline macro to be of type Inline; got #{replacement.class}: #{match})
339
+ logger.info { %(expected substitution value for custom inline macro to be of type Inline; got #{replacement.class}: #{match}) }
340
340
  replacement
341
341
  else
342
342
  ''
@@ -445,23 +445,16 @@ module Substitutors
445
445
  # indexterm:[Tigers,Big cats]
446
446
  if (attrlist = normalize_text $2, true, true).include? '='
447
447
  if (primary = (attrs = (AttributeList.new attrlist, self).parse)[1])
448
- attrs['terms'] = terms = [primary]
449
- if (secondary = attrs[2])
450
- terms << secondary
451
- if (tertiary = attrs[3])
452
- terms << tertiary
453
- end
454
- end
448
+ attrs['terms'] = [primary]
455
449
  if (see_also = attrs['see-also'])
456
450
  attrs['see-also'] = (see_also.include? ',') ? (see_also.split ',').map {|it| it.lstrip } : [see_also]
457
451
  end
458
452
  else
459
- attrs = { 'terms' => (terms = attrlist) }
453
+ attrs = { 'terms' => attrlist }
460
454
  end
461
455
  else
462
- attrs = { 'terms' => (terms = split_simple_csv attrlist) }
456
+ attrs = { 'terms' => (split_simple_csv attrlist) }
463
457
  end
464
- #doc.register :indexterms, terms
465
458
  (Inline.new self, :indexterm, nil, attributes: attrs).convert
466
459
  when 'indexterm2'
467
460
  # honor the escape
@@ -474,34 +467,33 @@ module Substitutors
474
467
  attrs['see-also'] = (see_also.include? ',') ? (see_also.split ',').map {|it| it.lstrip } : [see_also]
475
468
  end
476
469
  end
477
- #doc.register :indexterms, [term]
478
470
  (Inline.new self, :indexterm, term, attributes: attrs, type: :visible).convert
479
471
  else
480
- text = $3
472
+ encl_text = $3
481
473
  # honor the escape
482
474
  if $&.start_with? RS
483
475
  # escape concealed index term, but process nested flow index term
484
- if (text.start_with? '(') && (text.end_with? ')')
485
- text = text.slice 1, text.length - 2
476
+ if (encl_text.start_with? '(') && (encl_text.end_with? ')')
477
+ encl_text = encl_text.slice 1, encl_text.length - 2
486
478
  visible, before, after = true, '(', ')'
487
479
  else
488
480
  next $&.slice 1, $&.length
489
481
  end
490
482
  else
491
483
  visible = true
492
- if text.start_with? '('
493
- if text.end_with? ')'
494
- text, visible = (text.slice 1, text.length - 2), false
484
+ if encl_text.start_with? '('
485
+ if encl_text.end_with? ')'
486
+ encl_text, visible = (encl_text.slice 1, encl_text.length - 2), false
495
487
  else
496
- text, before, after = (text.slice 1, text.length), '(', ''
488
+ encl_text, before, after = (encl_text.slice 1, encl_text.length), '(', ''
497
489
  end
498
- elsif text.end_with? ')'
499
- text, before, after = text.chop, '', ')'
490
+ elsif encl_text.end_with? ')'
491
+ encl_text, before, after = encl_text.chop, '', ')'
500
492
  end
501
493
  end
502
494
  if visible
503
495
  # ((Tigers))
504
- if (term = normalize_text text, true).include? ';&'
496
+ if (term = normalize_text encl_text, true).include? ';&'
505
497
  if term.include? ' &gt;&gt; '
506
498
  term, _, see = term.partition ' &gt;&gt; '
507
499
  attrs = { 'see' => see }
@@ -510,12 +502,11 @@ module Substitutors
510
502
  attrs = { 'see-also' => see_also }
511
503
  end
512
504
  end
513
- #doc.register :indexterms, [term]
514
505
  subbed_term = (Inline.new self, :indexterm, term, attributes: attrs, type: :visible).convert
515
506
  else
516
507
  # (((Tigers,Big cats)))
517
508
  attrs = {}
518
- if (terms = normalize_text text, true).include? ';&'
509
+ if (terms = normalize_text encl_text, true).include? ';&'
519
510
  if terms.include? ' &gt;&gt; '
520
511
  terms, _, see = terms.partition ' &gt;&gt; '
521
512
  attrs['see'] = see
@@ -524,8 +515,7 @@ module Substitutors
524
515
  attrs['see-also'] = see_also
525
516
  end
526
517
  end
527
- attrs['terms'] = terms = split_simple_csv terms
528
- #doc.register :indexterms, terms
518
+ attrs['terms'] = split_simple_csv terms
529
519
  subbed_term = (Inline.new self, :indexterm, nil, attributes: attrs).convert
530
520
  end
531
521
  before ? %(#{before}#{subbed_term}#{after}) : subbed_term
@@ -545,7 +535,7 @@ module Substitutors
545
535
  # NOTE if $4 is set, we're looking at a formal macro (e.g., https://example.org[])
546
536
  if $4
547
537
  prefix = '' if prefix == 'link:'
548
- text = $4
538
+ link_text = nil if (link_text = $4).empty?
549
539
  else
550
540
  # invalid macro syntax (link: prefix w/o trailing square brackets or enclosed in double quotes)
551
541
  # FIXME we probably shouldn't even get here when the link: prefix is present; the regex is doing too much
@@ -553,12 +543,13 @@ module Substitutors
553
543
  when 'link:', ?", ?'
554
544
  next $&
555
545
  end
556
- text = ''
557
546
  case $3
558
- when ')'
559
- # move trailing ) out of URL
547
+ when ')', '?', '!'
560
548
  target = target.chop
561
- suffix = ')'
549
+ if (suffix = $3) == ')' && (target.end_with? '.', '?', '!')
550
+ suffix = target[-1] + suffix
551
+ target = target.chop
552
+ end
562
553
  # NOTE handle case when modified target is a URI scheme (e.g., http://)
563
554
  next $& if target.end_with? '://'
564
555
  when ';'
@@ -591,27 +582,37 @@ module Substitutors
591
582
  end
592
583
 
593
584
  attrs, link_opts = nil, { type: :link }
594
- unless text.empty?
595
- text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
596
- if !doc.compat_mode && (text.include? '=')
597
- # NOTE if an equals sign (=) is present, extract attributes from text
598
- text, attrs = extract_attributes_from_text text, ''
585
+
586
+ if link_text
587
+ new_link_text = link_text = link_text.gsub ESC_R_SB, R_SB if link_text.include? R_SB
588
+ if !doc.compat_mode && (link_text.include? '=')
589
+ # NOTE if an equals sign (=) is present, extract attributes from link text
590
+ link_text, attrs = extract_attributes_from_text link_text, ''
591
+ new_link_text = link_text
599
592
  link_opts[:id] = attrs['id']
600
593
  end
601
594
 
602
- if text.end_with? '^'
603
- text = text.chop
595
+ if link_text.end_with? '^'
596
+ new_link_text = link_text = link_text.chop
604
597
  if attrs
605
598
  attrs['window'] ||= '_blank'
606
599
  else
607
600
  attrs = { 'window' => '_blank' }
608
601
  end
609
602
  end
610
- end
611
603
 
612
- if text.empty?
604
+ if new_link_text && new_link_text.empty?
605
+ # NOTE it's not possible for the URI scheme to be bare in this case
606
+ link_text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
607
+ bare = true
608
+ end
609
+ else
613
610
  # NOTE it's not possible for the URI scheme to be bare in this case
614
- text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
611
+ link_text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
612
+ bare = true
613
+ end
614
+
615
+ if bare
615
616
  if attrs
616
617
  attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
617
618
  else
@@ -621,7 +622,7 @@ module Substitutors
621
622
 
622
623
  doc.register :links, (link_opts[:target] = target)
623
624
  link_opts[:attributes] = attrs if attrs
624
- %(#{prefix}#{(Inline.new self, :anchor, text, link_opts).convert}#{suffix})
625
+ %(#{prefix}#{(Inline.new self, :anchor, link_text, link_opts).convert}#{suffix})
625
626
  end
626
627
  end
627
628
 
@@ -637,12 +638,12 @@ module Substitutors
637
638
  target = $2
638
639
  end
639
640
  attrs, link_opts = nil, { type: :link }
640
- unless (text = $3).empty?
641
- text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
641
+ unless (link_text = $3).empty?
642
+ link_text = link_text.gsub ESC_R_SB, R_SB if link_text.include? R_SB
642
643
  if mailto
643
- if !doc.compat_mode && (text.include? ',')
644
- # NOTE if a comma (,) is present, extract attributes from text
645
- text, attrs = extract_attributes_from_text text, ''
644
+ if !doc.compat_mode && (link_text.include? ',')
645
+ # NOTE if a comma (,) is present, extract attributes from link text
646
+ link_text, attrs = extract_attributes_from_text link_text, ''
646
647
  link_opts[:id] = attrs['id']
647
648
  if attrs.key? 2
648
649
  if attrs.key? 3
@@ -652,14 +653,14 @@ module Substitutors
652
653
  end
653
654
  end
654
655
  end
655
- elsif !doc.compat_mode && (text.include? '=')
656
- # NOTE if an equals sign (=) is present, extract attributes from text
657
- text, attrs = extract_attributes_from_text text, ''
656
+ elsif !doc.compat_mode && (link_text.include? '=')
657
+ # NOTE if an equals sign (=) is present, extract attributes from link text
658
+ link_text, attrs = extract_attributes_from_text link_text, ''
658
659
  link_opts[:id] = attrs['id']
659
660
  end
660
661
 
661
- if text.end_with? '^'
662
- text = text.chop
662
+ if link_text.end_with? '^'
663
+ link_text = link_text.chop
663
664
  if attrs
664
665
  attrs['window'] ||= '_blank'
665
666
  else
@@ -668,17 +669,17 @@ module Substitutors
668
669
  end
669
670
  end
670
671
 
671
- if text.empty?
672
+ if link_text.empty?
672
673
  # mailto is a special case, already processed
673
674
  if mailto
674
- text = mailto_text
675
+ link_text = mailto_text
675
676
  else
676
677
  if doc_attrs.key? 'hide-uri-scheme'
677
- if (text = target.sub UriSniffRx, '').empty?
678
- text = target
678
+ if (link_text = target.sub UriSniffRx, '').empty?
679
+ link_text = target
679
680
  end
680
681
  else
681
- text = target
682
+ link_text = target
682
683
  end
683
684
  if attrs
684
685
  attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
@@ -691,7 +692,7 @@ module Substitutors
691
692
  # QUESTION should a mailto be registered as an e-mail address?
692
693
  doc.register :links, (link_opts[:target] = target)
693
694
  link_opts[:attributes] = attrs if attrs
694
- Inline.new(self, :anchor, text, link_opts).convert
695
+ Inline.new(self, :anchor, link_text, link_opts).convert
695
696
  end
696
697
  end
697
698
 
@@ -738,15 +739,17 @@ module Substitutors
738
739
 
739
740
  attrs = {}
740
741
  if (refid = $1)
741
- refid, text = refid.split ',', 2
742
- text = text.lstrip if text
742
+ if refid.include? ','
743
+ refid, _, link_text = refid.partition ','
744
+ link_text = nil if (link_text = link_text.lstrip).empty?
745
+ end
743
746
  else
744
747
  macro = true
745
748
  refid = $2
746
- if (text = $3)
747
- text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
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? '=')
749
+ if (link_text = $3)
750
+ link_text = link_text.gsub ESC_R_SB, R_SB if link_text.include? R_SB
751
+ # NOTE if an equals sign (=) is present, extract attributes from link text
752
+ link_text, attrs = extract_attributes_from_text link_text if !doc.compat_mode && (link_text.include? '=')
750
753
  end
751
754
  end
752
755
 
@@ -800,7 +803,7 @@ module Substitutors
800
803
  refid, path, target = nil, nil, '#'
801
804
  end
802
805
  else
803
- refid, path = path, %(#{doc.attributes['relfileprefix']}#{path}#{src2src ? (doc.attributes.fetch 'relfilesuffix', doc.outfilesuffix) : ''})
806
+ refid, path = path, %(#{doc.attributes['relfileprefix'] || ''}#{path}#{src2src ? (doc.attributes.fetch 'relfilesuffix', doc.outfilesuffix) : ''})
804
807
  if fragment
805
808
  refid, target = %(#{refid}##{fragment}), %(#{path}##{fragment})
806
809
  else
@@ -825,7 +828,7 @@ module Substitutors
825
828
  attrs['path'] = path
826
829
  attrs['fragment'] = fragment
827
830
  attrs['refid'] = refid
828
- Inline.new(self, :anchor, text, type: :xref, target: target, attributes: attrs).convert
831
+ Inline.new(self, :anchor, link_text, type: :xref, target: target, attributes: attrs).convert
829
832
  end
830
833
  end
831
834
 
@@ -837,7 +840,7 @@ module Substitutors
837
840
  # footnoteref
838
841
  if $1
839
842
  if $3
840
- id, text = $3.split ',', 2
843
+ id, content = $3.split ',', 2
841
844
  logger.warn %(found deprecated footnoteref macro: #{$&}; use footnote macro with target instead) unless doc.compat_mode
842
845
  else
843
846
  next $&
@@ -845,31 +848,31 @@ module Substitutors
845
848
  # footnote
846
849
  else
847
850
  id = $2
848
- text = $3
851
+ content = $3
849
852
  end
850
853
 
851
854
  if id
852
855
  if (footnote = doc.footnotes.find {|candidate| candidate.id == id })
853
- index, text = footnote.index, footnote.text
856
+ index, content = footnote.index, footnote.text
854
857
  type, target, id = :xref, id, nil
855
- elsif text
856
- text = restore_passthroughs(normalize_text text, true, true)
858
+ elsif content
859
+ content = restore_passthroughs(normalize_text content, true, true)
857
860
  index = doc.counter('footnote-number')
858
- doc.register(:footnotes, Document::Footnote.new(index, id, text))
861
+ doc.register(:footnotes, Document::Footnote.new(index, id, content))
859
862
  type, target = :ref, nil
860
863
  else
861
864
  logger.warn %(invalid footnote reference: #{id})
862
- type, target, text, id = :xref, id, id, nil
865
+ type, target, content, id = :xref, id, id, nil
863
866
  end
864
- elsif text
865
- text = restore_passthroughs(normalize_text text, true, true)
867
+ elsif content
868
+ content = restore_passthroughs(normalize_text content, true, true)
866
869
  index = doc.counter('footnote-number')
867
- doc.register(:footnotes, Document::Footnote.new(index, id, text))
870
+ doc.register(:footnotes, Document::Footnote.new(index, id, content))
868
871
  type = target = nil
869
872
  else
870
873
  next $&
871
874
  end
872
- Inline.new(self, :footnote, text, attributes: { 'index' => index }, id: id, target: target, type: type).convert
875
+ Inline.new(self, :footnote, content, attributes: { 'index' => index }, id: id, target: target, type: type).convert
873
876
  end
874
877
  end
875
878
 
@@ -946,8 +949,9 @@ module Substitutors
946
949
 
947
950
  doc_attrs = @document.attributes
948
951
  syntax_hl_name = syntax_hl.name
949
- if (linenums_mode = (attr? 'linenums') ? (doc_attrs[%(#{syntax_hl_name}-linenums-mode)] || :table).to_sym : nil)
950
- start_line_number = 1 if (start_line_number = (attr 'start', 1).to_i) < 1
952
+ if (linenums_mode = (attr? 'linenums') ? (doc_attrs[%(#{syntax_hl_name}-linenums-mode)] || :table).to_sym : nil) &&
953
+ (start_line_number = (attr 'start', 1).to_i) < 1
954
+ start_line_number = 1
951
955
  end
952
956
  highlight_lines = resolve_lines_to_highlight source, (attr 'highlight'), start_line_number if attr? 'highlight'
953
957
 
@@ -984,7 +988,7 @@ module Substitutors
984
988
  negate = true
985
989
  end
986
990
  if (delim = (entry.include? '..') ? '..' : ((entry.include? '-') ? '-' : nil))
987
- from, delim, to = entry.partition delim
991
+ from, _, to = entry.partition delim
988
992
  to = (source.count LF) + 1 if to.empty? || (to = to.to_i) < 0
989
993
  if negate
990
994
  lines -= (from.to_i..to).to_a
@@ -1118,7 +1122,7 @@ module Substitutors
1118
1122
  end
1119
1123
  subs = $2
1120
1124
  content = normalize_text $3, nil, true
1121
- # NOTE drop enclosing $ signs around latexmath for backwards compatibility with AsciiDoc Python
1125
+ # NOTE drop enclosing $ signs around latexmath for backwards compatibility with AsciiDoc.py
1122
1126
  content = content.slice 1, content.length - 2 if type == :latexmath && (content.start_with? '$') && (content.end_with? '$')
1123
1127
  subs = subs ? (resolve_pass_subs subs) : ((@document.basebackend? 'html') ? BASIC_SUBS : nil)
1124
1128
  passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, type: type }
@@ -1232,17 +1236,17 @@ module Substitutors
1232
1236
  resolve_subs subs, :inline, nil, 'passthrough macro'
1233
1237
  end
1234
1238
 
1235
- # Public: Expand all groups in the subs list and return. If no subs are resolve, return nil.
1239
+ # Public: Expand all groups in the subs list and return. If no subs are resolved, return nil.
1236
1240
  #
1237
- # subs - The substitutions to expand; can be a Symbol, Symbol Array or nil
1241
+ # subs - The substitutions to expand; can be a Symbol, Symbol Array, or String
1242
+ # subject - The String to use in log messages to communicate the subject for which subs are being resolved (default: nil)
1238
1243
  #
1239
1244
  # Returns a Symbol Array of substitutions to pass to apply_subs or nil if no substitutions were resolved.
1240
- def expand_subs subs
1241
- if ::Symbol === subs
1242
- unless subs == :none
1243
- SUB_GROUPS[subs] || [subs]
1244
- end
1245
- else
1245
+ def expand_subs subs, subject = nil
1246
+ case subs
1247
+ when ::Symbol
1248
+ subs == :none ? nil : SUB_GROUPS[subs] || [subs]
1249
+ when ::Array
1246
1250
  expanded_subs = []
1247
1251
  subs.each do |key|
1248
1252
  unless key == :none
@@ -1253,8 +1257,9 @@ module Substitutors
1253
1257
  end
1254
1258
  end
1255
1259
  end
1256
-
1257
1260
  expanded_subs.empty? ? nil : expanded_subs
1261
+ else
1262
+ resolve_subs subs, :inline, nil, subject
1258
1263
  end
1259
1264
  end
1260
1265
 
@@ -1276,7 +1281,7 @@ module Substitutors
1276
1281
  # NOTE :literal with listparagraph-option gets folded into text of list item later
1277
1282
  default_subs = @context == :verse ? NORMAL_SUBS : VERBATIM_SUBS
1278
1283
  when :raw
1279
- # TODO make pass subs a compliance setting; AsciiDoc Python performs :attributes and :macros on a pass block
1284
+ # TODO make pass subs a compliance setting; AsciiDoc.py performs :attributes and :macros on a pass block
1280
1285
  default_subs = @context == :stem ? BASIC_SUBS : NO_SUBS
1281
1286
  else
1282
1287
  return @subs
@@ -1472,33 +1477,28 @@ module Substitutors
1472
1477
  #
1473
1478
  # Returns a Hash of attributes (role and id only)
1474
1479
  def parse_quoted_text_attributes str
1475
- return {} if (str = str.rstrip).empty?
1476
1480
  # NOTE attributes are typically resolved after quoted text, so substitute eagerly
1477
1481
  str = sub_attributes str if str.include? ATTR_REF_HEAD
1478
1482
  # for compliance, only consider first positional attribute (very unlikely)
1479
1483
  str = str.slice 0, (str.index ',') if str.include? ','
1480
-
1481
- if (str.start_with? '.', '#') && Compliance.shorthand_property_syntax
1482
- segments = str.split '#', 2
1483
-
1484
- if segments.size > 1
1485
- id, *more_roles = segments[1].split('.')
1484
+ if (str = str.strip).empty?
1485
+ {}
1486
+ elsif (str.start_with? '.', '#') && Compliance.shorthand_property_syntax
1487
+ before, _, after = str.partition '#'
1488
+ attrs = {}
1489
+ if after.empty?
1490
+ attrs['role'] = (before.tr '.', ' ').lstrip if before.length > 1
1486
1491
  else
1487
- more_roles = []
1488
- end
1489
-
1490
- roles = segments[0].empty? ? [] : segments[0].split('.')
1491
- if roles.size > 1
1492
- roles.shift
1493
- end
1494
-
1495
- if more_roles.size > 0
1496
- roles.concat more_roles
1492
+ id, _, roles = after.partition '.'
1493
+ attrs['id'] = id unless id.empty?
1494
+ if roles.empty?
1495
+ attrs['role'] = (before.tr '.', ' ').lstrip if before.length > 1
1496
+ elsif before.length > 1
1497
+ attrs['role'] = ((before + '.' + roles).tr '.', ' ').lstrip
1498
+ else
1499
+ attrs['role'] = roles.tr '.', ' '
1500
+ end
1497
1501
  end
1498
-
1499
- attrs = {}
1500
- attrs['id'] = id if id
1501
- attrs['role'] = roles.join ' ' unless roles.empty?
1502
1502
  attrs
1503
1503
  else
1504
1504
  { 'role' => str }
@@ -1532,7 +1532,7 @@ module Substitutors
1532
1532
  case c
1533
1533
  when ','
1534
1534
  if quote_open
1535
- accum = accum + c
1535
+ accum += c
1536
1536
  else
1537
1537
  values << accum.strip
1538
1538
  accum = ''
@@ -1540,7 +1540,7 @@ module Substitutors
1540
1540
  when '"'
1541
1541
  quote_open = !quote_open
1542
1542
  else
1543
- accum = accum + c
1543
+ accum += c
1544
1544
  end
1545
1545
  end
1546
1546
  values << accum.strip