asciidoctor 2.0.12 → 2.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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