asciidoctor 2.0.10 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +294 -30
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +16 -20
  5. data/README-fr.adoc +15 -22
  6. data/README-jp.adoc +15 -26
  7. data/README-zh_CN.adoc +21 -25
  8. data/README.adoc +161 -138
  9. data/asciidoctor.gemspec +6 -13
  10. data/data/locale/attributes-ar.adoc +4 -3
  11. data/data/locale/attributes-be.adoc +23 -0
  12. data/data/locale/attributes-bg.adoc +4 -3
  13. data/data/locale/attributes-ca.adoc +6 -5
  14. data/data/locale/attributes-cs.adoc +4 -3
  15. data/data/locale/attributes-da.adoc +6 -5
  16. data/data/locale/attributes-de.adoc +4 -4
  17. data/data/locale/attributes-en.adoc +4 -4
  18. data/data/locale/attributes-es.adoc +6 -5
  19. data/data/locale/attributes-fa.adoc +4 -3
  20. data/data/locale/attributes-fi.adoc +4 -3
  21. data/data/locale/attributes-fr.adoc +8 -7
  22. data/data/locale/attributes-hu.adoc +4 -3
  23. data/data/locale/attributes-id.adoc +4 -3
  24. data/data/locale/attributes-it.adoc +6 -5
  25. data/data/locale/attributes-ja.adoc +4 -3
  26. data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
  27. data/data/locale/attributes-nb.adoc +4 -3
  28. data/data/locale/attributes-nl.adoc +6 -5
  29. data/data/locale/attributes-nn.adoc +4 -3
  30. data/data/locale/attributes-pl.adoc +8 -7
  31. data/data/locale/attributes-pt.adoc +6 -5
  32. data/data/locale/attributes-pt_BR.adoc +6 -5
  33. data/data/locale/attributes-ro.adoc +4 -3
  34. data/data/locale/attributes-ru.adoc +6 -5
  35. data/data/locale/attributes-sr.adoc +4 -4
  36. data/data/locale/attributes-sr_Latn.adoc +4 -4
  37. data/data/locale/attributes-sv.adoc +4 -4
  38. data/data/locale/attributes-th.adoc +23 -0
  39. data/data/locale/attributes-tr.adoc +4 -3
  40. data/data/locale/attributes-uk.adoc +6 -5
  41. data/data/locale/attributes-vi.adoc +23 -0
  42. data/data/locale/attributes-zh_CN.adoc +4 -3
  43. data/data/locale/attributes-zh_TW.adoc +4 -3
  44. data/data/reference/syntax.adoc +14 -7
  45. data/data/stylesheets/asciidoctor-default.css +76 -76
  46. data/data/stylesheets/coderay-asciidoctor.css +9 -9
  47. data/lib/asciidoctor/abstract_block.rb +20 -13
  48. data/lib/asciidoctor/abstract_node.rb +23 -12
  49. data/lib/asciidoctor/attribute_list.rb +64 -72
  50. data/lib/asciidoctor/block.rb +6 -6
  51. data/lib/asciidoctor/cli/invoker.rb +3 -2
  52. data/lib/asciidoctor/cli/options.rb +32 -31
  53. data/lib/asciidoctor/convert.rb +168 -162
  54. data/lib/asciidoctor/converter/docbook5.rb +49 -34
  55. data/lib/asciidoctor/converter/html5.rb +180 -139
  56. data/lib/asciidoctor/converter/manpage.rb +118 -90
  57. data/lib/asciidoctor/converter/template.rb +15 -13
  58. data/lib/asciidoctor/converter.rb +19 -16
  59. data/lib/asciidoctor/core_ext/hash/merge.rb +1 -1
  60. data/lib/asciidoctor/document.rb +77 -86
  61. data/lib/asciidoctor/extensions.rb +22 -16
  62. data/lib/asciidoctor/helpers.rb +20 -15
  63. data/lib/asciidoctor/list.rb +2 -6
  64. data/lib/asciidoctor/load.rb +103 -101
  65. data/lib/asciidoctor/logging.rb +10 -8
  66. data/lib/asciidoctor/parser.rb +211 -220
  67. data/lib/asciidoctor/path_resolver.rb +17 -15
  68. data/lib/asciidoctor/reader.rb +87 -79
  69. data/lib/asciidoctor/rx.rb +9 -7
  70. data/lib/asciidoctor/section.rb +7 -0
  71. data/lib/asciidoctor/substitutors.rb +167 -148
  72. data/lib/asciidoctor/syntax_highlighter/coderay.rb +3 -2
  73. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +13 -5
  74. data/lib/asciidoctor/syntax_highlighter/prettify.rb +7 -4
  75. data/lib/asciidoctor/syntax_highlighter/pygments.rb +19 -11
  76. data/lib/asciidoctor/syntax_highlighter/rouge.rb +35 -20
  77. data/lib/asciidoctor/syntax_highlighter.rb +16 -16
  78. data/lib/asciidoctor/table.rb +70 -43
  79. data/lib/asciidoctor/timings.rb +3 -3
  80. data/lib/asciidoctor/version.rb +1 -1
  81. data/lib/asciidoctor.rb +45 -19
  82. data/man/asciidoctor.1 +29 -31
  83. data/man/asciidoctor.adoc +35 -29
  84. metadata +17 -70
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Asciidoctor
3
3
  # Public: Methods to perform substitutions on lines of AsciiDoc text. This module
4
- # is intented to be mixed-in to Section and Block to provide operations for performing
4
+ # is intended to be mixed-in to Section and Block to provide operations for performing
5
5
  # the necessary substitutions.
6
6
  module Substitutors
7
7
  SpecialCharsRx = /[<&>]/
@@ -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
@@ -542,20 +532,24 @@ module Substitutors
542
532
  end
543
533
 
544
534
  prefix, suffix = $1, ''
545
- # NOTE if $4 is set, then we're looking at a formal macro
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
- # 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:'
553
- text = ''
540
+ # invalid macro syntax (link: prefix w/o trailing square brackets or enclosed in double quotes)
541
+ # FIXME we probably shouldn't even get here when the link: prefix is present; the regex is doing too much
542
+ case prefix
543
+ when 'link:', ?", ?'
544
+ next $&
545
+ end
554
546
  case $3
555
- when ')'
556
- # move trailing ) out of URL
547
+ when ')', '?', '!'
557
548
  target = target.chop
558
- suffix = ')'
549
+ if (suffix = $3) == ')' && (target.end_with? '.', '?', '!')
550
+ suffix = target[-1] + suffix
551
+ target = target.chop
552
+ end
559
553
  # NOTE handle case when modified target is a URI scheme (e.g., http://)
560
554
  next $& if target.end_with? '://'
561
555
  when ';'
@@ -588,26 +582,37 @@ module Substitutors
588
582
  end
589
583
 
590
584
  attrs, link_opts = nil, { type: :link }
591
- unless text.empty?
592
- text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
593
- if !doc.compat_mode && (text.include? '=')
594
- text = (attrs = (AttributeList.new text, self).parse)[1] || ''
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
595
592
  link_opts[:id] = attrs['id']
596
593
  end
597
594
 
598
- if text.end_with? '^'
599
- text = text.chop
595
+ if link_text.end_with? '^'
596
+ new_link_text = link_text = link_text.chop
600
597
  if attrs
601
598
  attrs['window'] ||= '_blank'
602
599
  else
603
600
  attrs = { 'window' => '_blank' }
604
601
  end
605
602
  end
606
- end
607
603
 
608
- 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
609
610
  # NOTE it's not possible for the URI scheme to be bare in this case
610
- 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
611
616
  if attrs
612
617
  attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
613
618
  else
@@ -617,7 +622,7 @@ module Substitutors
617
622
 
618
623
  doc.register :links, (link_opts[:target] = target)
619
624
  link_opts[:attributes] = attrs if attrs
620
- %(#{prefix}#{(Inline.new self, :anchor, text, link_opts).convert}#{suffix})
625
+ %(#{prefix}#{(Inline.new self, :anchor, link_text, link_opts).convert}#{suffix})
621
626
  end
622
627
  end
623
628
 
@@ -633,11 +638,12 @@ module Substitutors
633
638
  target = $2
634
639
  end
635
640
  attrs, link_opts = nil, { type: :link }
636
- unless (text = $3).empty?
637
- 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
638
643
  if mailto
639
- if !doc.compat_mode && (text.include? ',')
640
- text = (attrs = (AttributeList.new text, self).parse)[1] || ''
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, ''
641
647
  link_opts[:id] = attrs['id']
642
648
  if attrs.key? 2
643
649
  if attrs.key? 3
@@ -647,13 +653,14 @@ module Substitutors
647
653
  end
648
654
  end
649
655
  end
650
- elsif !doc.compat_mode && (text.include? '=')
651
- text = (attrs = (AttributeList.new text, self).parse)[1] || ''
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, ''
652
659
  link_opts[:id] = attrs['id']
653
660
  end
654
661
 
655
- if text.end_with? '^'
656
- text = text.chop
662
+ if link_text.end_with? '^'
663
+ link_text = link_text.chop
657
664
  if attrs
658
665
  attrs['window'] ||= '_blank'
659
666
  else
@@ -662,17 +669,17 @@ module Substitutors
662
669
  end
663
670
  end
664
671
 
665
- if text.empty?
672
+ if link_text.empty?
666
673
  # mailto is a special case, already processed
667
674
  if mailto
668
- text = mailto_text
675
+ link_text = mailto_text
669
676
  else
670
677
  if doc_attrs.key? 'hide-uri-scheme'
671
- if (text = target.sub UriSniffRx, '').empty?
672
- text = target
678
+ if (link_text = target.sub UriSniffRx, '').empty?
679
+ link_text = target
673
680
  end
674
681
  else
675
- text = target
682
+ link_text = target
676
683
  end
677
684
  if attrs
678
685
  attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
@@ -685,7 +692,7 @@ module Substitutors
685
692
  # QUESTION should a mailto be registered as an e-mail address?
686
693
  doc.register :links, (link_opts[:target] = target)
687
694
  link_opts[:attributes] = attrs if attrs
688
- Inline.new(self, :anchor, text, link_opts).convert
695
+ Inline.new(self, :anchor, link_text, link_opts).convert
689
696
  end
690
697
  end
691
698
 
@@ -732,15 +739,17 @@ module Substitutors
732
739
 
733
740
  attrs = {}
734
741
  if (refid = $1)
735
- refid, text = refid.split ',', 2
736
- 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
737
746
  else
738
747
  macro = true
739
748
  refid = $2
740
- if (text = $3)
741
- 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? '=')
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? '=')
744
753
  end
745
754
  end
746
755
 
@@ -794,7 +803,7 @@ module Substitutors
794
803
  refid, path, target = nil, nil, '#'
795
804
  end
796
805
  else
797
- 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) : ''})
798
807
  if fragment
799
808
  refid, target = %(#{refid}##{fragment}), %(#{path}##{fragment})
800
809
  else
@@ -804,7 +813,7 @@ module Substitutors
804
813
  # handles: id (in compat mode or when natural xrefs are disabled)
805
814
  elsif doc.compat_mode || !Compliance.natural_xrefs
806
815
  refid, target = fragment, %(##{fragment})
807
- logger.info %(possible invalid reference: #{refid}) if logger.info? && doc.catalog[:refs][refid]
816
+ logger.info %(possible invalid reference: #{refid}) if logger.info? && !doc.catalog[:refs][refid]
808
817
  # handles: id
809
818
  elsif doc.catalog[:refs][fragment]
810
819
  refid, target = fragment, %(##{fragment})
@@ -819,7 +828,7 @@ module Substitutors
819
828
  attrs['path'] = path
820
829
  attrs['fragment'] = fragment
821
830
  attrs['refid'] = refid
822
- 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
823
832
  end
824
833
  end
825
834
 
@@ -831,7 +840,7 @@ module Substitutors
831
840
  # footnoteref
832
841
  if $1
833
842
  if $3
834
- id, text = $3.split ',', 2
843
+ id, content = $3.split ',', 2
835
844
  logger.warn %(found deprecated footnoteref macro: #{$&}; use footnote macro with target instead) unless doc.compat_mode
836
845
  else
837
846
  next $&
@@ -839,33 +848,31 @@ module Substitutors
839
848
  # footnote
840
849
  else
841
850
  id = $2
842
- text = $3
851
+ content = $3
843
852
  end
844
853
 
845
854
  if id
846
- if text
847
- text = restore_passthroughs(normalize_text text, true, true)
855
+ if (footnote = doc.footnotes.find {|candidate| candidate.id == id })
856
+ index, content = footnote.index, footnote.text
857
+ type, target, id = :xref, id, nil
858
+ elsif content
859
+ content = restore_passthroughs(normalize_text content, true, true)
848
860
  index = doc.counter('footnote-number')
849
- doc.register(:footnotes, Document::Footnote.new(index, id, text))
861
+ doc.register(:footnotes, Document::Footnote.new(index, id, content))
850
862
  type, target = :ref, nil
851
863
  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
864
+ logger.warn %(invalid footnote reference: #{id})
865
+ type, target, content, id = :xref, id, id, nil
859
866
  end
860
- elsif text
861
- text = restore_passthroughs(normalize_text text, true, true)
867
+ elsif content
868
+ content = restore_passthroughs(normalize_text content, true, true)
862
869
  index = doc.counter('footnote-number')
863
- doc.register(:footnotes, Document::Footnote.new(index, id, text))
870
+ doc.register(:footnotes, Document::Footnote.new(index, id, content))
864
871
  type = target = nil
865
872
  else
866
873
  next $&
867
874
  end
868
- 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
869
876
  end
870
877
  end
871
878
 
@@ -917,7 +924,7 @@ module Substitutors
917
924
  # use sub since it might be behind a line comment
918
925
  $&.sub RS, ''
919
926
  else
920
- Inline.new(self, :callout, $4 == '.' ? (autonum += 1).to_s : $4, id: @document.callouts.read_next_id, attributes: { 'guard' => $1 }).convert
927
+ Inline.new(self, :callout, $4 == '.' ? (autonum += 1).to_s : $4, id: @document.callouts.read_next_id, attributes: { 'guard' => $1 || ($3 == '--' ? ['<!--', '-->'] : nil) }).convert
921
928
  end
922
929
  end
923
930
  end
@@ -933,7 +940,7 @@ module Substitutors
933
940
  # process_callouts - a Boolean flag indicating whether callout marks should be located and substituted
934
941
  #
935
942
  # Returns the highlighted source code, if a syntax highlighter is defined on the document, otherwise the source with
936
- # verbatim substituions applied
943
+ # verbatim substitutions applied
937
944
  def highlight_source source, process_callouts
938
945
  # NOTE the call to highlight? is a defensive check since, normally, we wouldn't arrive here unless it returns true
939
946
  return sub_source source, process_callouts unless (syntax_hl = @document.syntax_highlighter) && syntax_hl.highlight?
@@ -942,10 +949,11 @@ module Substitutors
942
949
 
943
950
  doc_attrs = @document.attributes
944
951
  syntax_hl_name = syntax_hl.name
945
- if (linenums_mode = (attr? 'linenums') ? (doc_attrs[%(#{syntax_hl_name}-linenums-mode)] || :table).to_sym : nil)
946
- 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
947
955
  end
948
- highlight_lines = resolve_lines_to_highlight source, (attr 'highlight') if attr? 'highlight'
956
+ highlight_lines = resolve_lines_to_highlight source, (attr 'highlight'), start_line_number if attr? 'highlight'
949
957
 
950
958
  highlighted, source_offset = syntax_hl.highlight self, source, (attr 'language'),
951
959
  callouts: callout_marks,
@@ -968,9 +976,10 @@ module Substitutors
968
976
  #
969
977
  # source - The String source.
970
978
  # spec - The lines specifier (e.g., "1-5, !2, 10" or "1..5;!2;10")
979
+ # start - The line number of the first line (optional, default: false)
971
980
  #
972
981
  # Returns an [Array] of unique, sorted line numbers.
973
- def resolve_lines_to_highlight source, spec
982
+ def resolve_lines_to_highlight source, spec, start = nil
974
983
  lines = []
975
984
  spec = spec.delete ' ' if spec.include? ' '
976
985
  ((spec.include? ',') ? (spec.split ',') : (spec.split ';')).map do |entry|
@@ -979,28 +988,29 @@ module Substitutors
979
988
  negate = true
980
989
  end
981
990
  if (delim = (entry.include? '..') ? '..' : ((entry.include? '-') ? '-' : nil))
982
- from, delim, to = entry.partition delim
991
+ from, _, to = entry.partition delim
983
992
  to = (source.count LF) + 1 if to.empty? || (to = to.to_i) < 0
984
- line_nums = (from.to_i..to).to_a
985
993
  if negate
986
- lines -= line_nums
994
+ lines -= (from.to_i..to).to_a
987
995
  else
988
- lines.concat line_nums
989
- end
990
- else
991
- if negate
992
- lines.delete entry.to_i
993
- else
994
- lines << entry.to_i
996
+ lines |= (from.to_i..to).to_a
995
997
  end
998
+ elsif negate
999
+ lines.delete entry.to_i
1000
+ elsif !lines.include?(line = entry.to_i)
1001
+ lines << line
996
1002
  end
997
1003
  end
998
- lines.sort.uniq
1004
+ # If the start attribute is defined, then the lines to highlight specified by the provided spec should be relative to the start value.
1005
+ unless (shift = start ? start - 1 : 0) == 0
1006
+ lines = lines.map {|it| it - shift }
1007
+ end
1008
+ lines.sort
999
1009
  end
1000
1010
 
1001
1011
  # Public: Extract the passthrough text from the document for reinsertion after processing.
1002
1012
  #
1003
- # text - The String from which to extract passthrough fragements
1013
+ # text - The String from which to extract passthrough fragments
1004
1014
  #
1005
1015
  # Returns the String text with passthrough regions substituted with placeholders
1006
1016
  def extract_passthroughs text
@@ -1112,7 +1122,7 @@ module Substitutors
1112
1122
  end
1113
1123
  subs = $2
1114
1124
  content = normalize_text $3, nil, true
1115
- # 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
1116
1126
  content = content.slice 1, content.length - 2 if type == :latexmath && (content.start_with? '$') && (content.end_with? '$')
1117
1127
  subs = subs ? (resolve_pass_subs subs) : ((@document.basebackend? 'html') ? BASIC_SUBS : nil)
1118
1128
  passthrus[passthru_key = passthrus.size] = { text: content, subs: subs, type: type }
@@ -1207,7 +1217,7 @@ module Substitutors
1207
1217
  end
1208
1218
  end
1209
1219
  return unless candidates
1210
- # weed out invalid options and remove duplicates (order is preserved; first occurence wins)
1220
+ # weed out invalid options and remove duplicates (order is preserved; first occurrence wins)
1211
1221
  resolved = candidates & SUB_OPTIONS[type]
1212
1222
  unless (candidates - resolved).empty?
1213
1223
  invalid = candidates - resolved
@@ -1226,17 +1236,17 @@ module Substitutors
1226
1236
  resolve_subs subs, :inline, nil, 'passthrough macro'
1227
1237
  end
1228
1238
 
1229
- # 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.
1230
1240
  #
1231
- # 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)
1232
1243
  #
1233
1244
  # Returns a Symbol Array of substitutions to pass to apply_subs or nil if no substitutions were resolved.
1234
- def expand_subs subs
1235
- if ::Symbol === subs
1236
- unless subs == :none
1237
- SUB_GROUPS[subs] || [subs]
1238
- end
1239
- 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
1240
1250
  expanded_subs = []
1241
1251
  subs.each do |key|
1242
1252
  unless key == :none
@@ -1247,8 +1257,9 @@ module Substitutors
1247
1257
  end
1248
1258
  end
1249
1259
  end
1250
-
1251
1260
  expanded_subs.empty? ? nil : expanded_subs
1261
+ else
1262
+ resolve_subs subs, :inline, nil, subject
1252
1263
  end
1253
1264
  end
1254
1265
 
@@ -1270,7 +1281,7 @@ module Substitutors
1270
1281
  # NOTE :literal with listparagraph-option gets folded into text of list item later
1271
1282
  default_subs = @context == :verse ? NORMAL_SUBS : VERBATIM_SUBS
1272
1283
  when :raw
1273
- # 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
1274
1285
  default_subs = @context == :stem ? BASIC_SUBS : NO_SUBS
1275
1286
  else
1276
1287
  return @subs
@@ -1321,10 +1332,23 @@ module Substitutors
1321
1332
 
1322
1333
  private
1323
1334
 
1335
+ # This method is used in cases when the attrlist can be mixed with the text of a macro.
1336
+ # If no attributes are detected aside from the first positional attribute, and the first positional
1337
+ # attribute matches the attrlist, then the original text is returned.
1338
+ def extract_attributes_from_text text, default_text = nil
1339
+ attrlist = (text.include? LF) ? (text.tr LF, ' ') : text
1340
+ if (resolved_text = (attrs = (AttributeList.new attrlist, self).parse)[1])
1341
+ # NOTE if resolved text remains unchanged, clear attributes and return unparsed text
1342
+ resolved_text == attrlist ? [text, attrs.clear] : [resolved_text, attrs]
1343
+ else
1344
+ [default_text, attrs]
1345
+ end
1346
+ end
1347
+
1324
1348
  # Internal: Extract the callout numbers from the source to prepare it for syntax highlighting.
1325
1349
  def extract_callouts source
1326
1350
  callout_marks = {}
1327
- lineno = 0
1351
+ autonum = lineno = 0
1328
1352
  last_lineno = nil
1329
1353
  callout_rx = (attr? 'line-comment') ? CalloutExtractRxMap[attr 'line-comment'] : CalloutExtractRx
1330
1354
  # extract callout marks, indexed by line number
@@ -1336,7 +1360,7 @@ module Substitutors
1336
1360
  # use sub since it might be behind a line comment
1337
1361
  $&.sub RS, ''
1338
1362
  else
1339
- (callout_marks[lineno] ||= []) << [$1, $4]
1363
+ (callout_marks[lineno] ||= []) << [$1 || ($3 == '--' ? ['<!--', '-->'] : nil), $4 == '.' ? (autonum += 1).to_s : $4]
1340
1364
  last_lineno = lineno
1341
1365
  ''
1342
1366
  end
@@ -1358,15 +1382,15 @@ module Substitutors
1358
1382
  else
1359
1383
  preamble = ''
1360
1384
  end
1361
- autonum = lineno = 0
1385
+ lineno = 0
1362
1386
  preamble + ((source.split LF, -1).map do |line|
1363
1387
  if (conums = callout_marks.delete lineno += 1)
1364
1388
  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})
1389
+ guard, numeral = conums[0]
1390
+ %(#{line}#{Inline.new(self, :callout, numeral, id: @document.callouts.read_next_id, attributes: { 'guard' => guard }).convert})
1367
1391
  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
1392
+ %(#{line}#{conums.map do |guard_it, numeral_it|
1393
+ Inline.new(self, :callout, numeral_it, id: @document.callouts.read_next_id, attributes: { 'guard' => guard_it }).convert
1370
1394
  end.join ' '})
1371
1395
  end
1372
1396
  else
@@ -1453,33 +1477,28 @@ module Substitutors
1453
1477
  #
1454
1478
  # Returns a Hash of attributes (role and id only)
1455
1479
  def parse_quoted_text_attributes str
1456
- return {} if (str = str.rstrip).empty?
1457
1480
  # NOTE attributes are typically resolved after quoted text, so substitute eagerly
1458
1481
  str = sub_attributes str if str.include? ATTR_REF_HEAD
1459
1482
  # for compliance, only consider first positional attribute (very unlikely)
1460
1483
  str = str.slice 0, (str.index ',') if str.include? ','
1461
-
1462
- if (str.start_with? '.', '#') && Compliance.shorthand_property_syntax
1463
- segments = str.split '#', 2
1464
-
1465
- if segments.size > 1
1466
- 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
1467
1491
  else
1468
- more_roles = []
1469
- end
1470
-
1471
- roles = segments[0].empty? ? [] : segments[0].split('.')
1472
- if roles.size > 1
1473
- roles.shift
1474
- end
1475
-
1476
- if more_roles.size > 0
1477
- 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
1478
1501
  end
1479
-
1480
- attrs = {}
1481
- attrs['id'] = id if id
1482
- attrs['role'] = roles.join ' ' unless roles.empty?
1483
1502
  attrs
1484
1503
  else
1485
1504
  { 'role' => str }
@@ -1513,7 +1532,7 @@ module Substitutors
1513
1532
  case c
1514
1533
  when ','
1515
1534
  if quote_open
1516
- accum = accum + c
1535
+ accum += c
1517
1536
  else
1518
1537
  values << accum.strip
1519
1538
  accum = ''
@@ -1521,7 +1540,7 @@ module Substitutors
1521
1540
  when '"'
1522
1541
  quote_open = !quote_open
1523
1542
  else
1524
- accum = accum + c
1543
+ accum += c
1525
1544
  end
1526
1545
  end
1527
1546
  values << accum.strip