asciidoctor 1.5.5 → 1.5.6

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of asciidoctor might be problematic. Click here for more details.

Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +216 -1
  3. data/CONTRIBUTING.adoc +2 -2
  4. data/Gemfile +20 -1
  5. data/LICENSE.adoc +1 -1
  6. data/README-fr.adoc +4 -3
  7. data/README-jp.adoc +11 -10
  8. data/README-zh_CN.adoc +4 -3
  9. data/README.adoc +17 -202
  10. data/Rakefile +41 -25
  11. data/asciidoctor.gemspec +9 -10
  12. data/data/locale/attributes.adoc +216 -34
  13. data/data/stylesheets/asciidoctor-default.css +23 -16
  14. data/features/step_definitions.rb +15 -19
  15. data/features/xref.feature +584 -20
  16. data/lib/asciidoctor.rb +292 -278
  17. data/lib/asciidoctor/abstract_block.rb +155 -94
  18. data/lib/asciidoctor/abstract_node.rb +108 -94
  19. data/lib/asciidoctor/attribute_list.rb +30 -22
  20. data/lib/asciidoctor/block.rb +7 -7
  21. data/lib/asciidoctor/cli/invoker.rb +47 -34
  22. data/lib/asciidoctor/cli/options.rb +22 -11
  23. data/lib/asciidoctor/converter.rb +3 -3
  24. data/lib/asciidoctor/converter/base.rb +2 -2
  25. data/lib/asciidoctor/converter/composite.rb +1 -1
  26. data/lib/asciidoctor/converter/docbook45.rb +2 -2
  27. data/lib/asciidoctor/converter/docbook5.rb +132 -87
  28. data/lib/asciidoctor/converter/factory.rb +0 -1
  29. data/lib/asciidoctor/converter/html5.rb +116 -98
  30. data/lib/asciidoctor/converter/manpage.rb +51 -52
  31. data/lib/asciidoctor/converter/template.rb +47 -36
  32. data/lib/asciidoctor/core_ext.rb +8 -2
  33. data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +4 -0
  34. data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +6 -0
  35. data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +5 -0
  36. data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +1 -1
  37. data/lib/asciidoctor/core_ext/1.8.7/string/{limit.rb → limit_bytesize.rb} +7 -6
  38. data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +6 -0
  39. data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +1 -1
  40. data/lib/asciidoctor/core_ext/nil_or_empty.rb +5 -5
  41. data/lib/asciidoctor/core_ext/regexp/is_match.rb +3 -0
  42. data/lib/asciidoctor/core_ext/string/{limit.rb → limit_bytesize.rb} +2 -2
  43. data/lib/asciidoctor/document.rb +216 -213
  44. data/lib/asciidoctor/extensions.rb +318 -185
  45. data/lib/asciidoctor/helpers.rb +35 -35
  46. data/lib/asciidoctor/inline.rb +32 -1
  47. data/lib/asciidoctor/list.rb +22 -6
  48. data/lib/asciidoctor/parser.rb +1008 -1038
  49. data/lib/asciidoctor/path_resolver.rb +46 -50
  50. data/lib/asciidoctor/reader.rb +275 -251
  51. data/lib/asciidoctor/section.rb +86 -58
  52. data/lib/asciidoctor/stylesheets.rb +6 -6
  53. data/lib/asciidoctor/substitutors.rb +567 -649
  54. data/lib/asciidoctor/table.rb +163 -108
  55. data/lib/asciidoctor/version.rb +1 -1
  56. data/man/asciidoctor.1 +18 -16
  57. data/man/asciidoctor.adoc +15 -13
  58. data/test/attributes_test.rb +138 -22
  59. data/test/blocks_test.rb +377 -97
  60. data/test/converter_test.rb +13 -0
  61. data/test/document_test.rb +244 -34
  62. data/test/extensions_test.rb +409 -42
  63. data/test/fixtures/asciidoc_index.txt +521 -0
  64. data/test/fixtures/basic-docinfo-footer.html +6 -0
  65. data/test/fixtures/basic-docinfo-footer.xml +8 -0
  66. data/test/fixtures/basic-docinfo.html +1 -0
  67. data/test/fixtures/basic-docinfo.xml +4 -0
  68. data/test/fixtures/basic.asciidoc +5 -0
  69. data/test/fixtures/chapter-a.adoc +3 -0
  70. data/test/fixtures/child-include.adoc +5 -0
  71. data/test/fixtures/circle.svg +9 -0
  72. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
  73. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
  74. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
  75. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
  76. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
  77. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
  78. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
  79. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
  80. data/test/fixtures/custom-docinfodir/basic-docinfo.html +1 -0
  81. data/test/fixtures/custom-docinfodir/docinfo.html +1 -0
  82. data/test/fixtures/docinfo-footer.html +1 -0
  83. data/test/fixtures/docinfo-footer.xml +9 -0
  84. data/test/fixtures/docinfo.html +1 -0
  85. data/test/fixtures/docinfo.xml +3 -0
  86. data/test/fixtures/dot.gif +0 -0
  87. data/test/fixtures/encoding.asciidoc +13 -0
  88. data/test/fixtures/grandchild-include.adoc +3 -0
  89. data/test/fixtures/hello-asciidoctor.pdf +69 -0
  90. data/test/fixtures/include-file.asciidoc +24 -0
  91. data/test/fixtures/include-file.ml +3 -0
  92. data/test/fixtures/include-file.xml +5 -0
  93. data/test/fixtures/master.adoc +5 -0
  94. data/test/fixtures/mismatched-end-tag.adoc +7 -0
  95. data/test/fixtures/parent-include-restricted.adoc +5 -0
  96. data/test/fixtures/parent-include.adoc +5 -0
  97. data/test/fixtures/sample.asciidoc +26 -0
  98. data/test/fixtures/stylesheets/custom.css +3 -0
  99. data/test/fixtures/subs-docinfo.html +2 -0
  100. data/test/fixtures/subs.adoc +7 -0
  101. data/test/fixtures/tagged-class-enclosed.rb +26 -0
  102. data/test/fixtures/tagged-class.rb +23 -0
  103. data/test/fixtures/tip.gif +0 -0
  104. data/test/invoker_test.rb +82 -4
  105. data/test/links_test.rb +312 -37
  106. data/test/lists_test.rb +204 -25
  107. data/test/manpage_test.rb +191 -4
  108. data/test/options_test.rb +18 -1
  109. data/test/paragraphs_test.rb +32 -7
  110. data/test/parser_test.rb +150 -30
  111. data/test/paths_test.rb +47 -13
  112. data/test/preamble_test.rb +1 -1
  113. data/test/reader_test.rb +366 -126
  114. data/test/sections_test.rb +203 -56
  115. data/test/substitutions_test.rb +339 -131
  116. data/test/tables_test.rb +315 -15
  117. data/test/test_helper.rb +400 -0
  118. data/test/text_test.rb +5 -5
  119. metadata +110 -22
@@ -24,10 +24,6 @@ class Section < AbstractBlock
24
24
  # Public: Get/Set the 0-based index order of this section within the parent block
25
25
  attr_accessor :index
26
26
 
27
- # Public: Get/Set the number of this section within the parent block
28
- # Only relevant if the attribute numbered is true
29
- attr_accessor :number
30
-
31
27
  # Public: Get/Set the section name of this section
32
28
  attr_accessor :sectname
33
29
 
@@ -37,16 +33,15 @@ class Section < AbstractBlock
37
33
  # Public: Get the state of the numbered attribute at this section (need to preserve for creating TOC)
38
34
  attr_accessor :numbered
39
35
 
36
+ # Public: Get the caption for this section (only relevant for appendices)
37
+ attr_reader :caption
38
+
40
39
  # Public: Initialize an Asciidoctor::Section object.
41
40
  #
42
41
  # parent - The parent Asciidoc Object.
43
42
  def initialize parent = nil, level = nil, numbered = true, opts = {}
44
43
  super parent, :section, opts
45
- if level
46
- @level = level
47
- else
48
- @level = parent ? (parent.level + 1) : 1
49
- end
44
+ @level = level ? level : (parent ? (parent.level + 1) : 1)
50
45
  @numbered = numbered && @level > 0
51
46
  @special = parent && parent.context == :section && parent.special
52
47
  @index = 0
@@ -54,54 +49,13 @@ class Section < AbstractBlock
54
49
  end
55
50
 
56
51
  # Public: The name of this section, an alias of the section title
57
- alias :name :title
52
+ alias name title
58
53
 
59
- # Public: Generate a String id for this section.
60
- #
61
- # The generated id is prefixed with value of the 'idprefix' attribute, which
62
- # is an underscore by default.
63
- #
64
- # Section id synthesis can be disabled by undefining the 'sectids' attribute.
54
+ # Public: Generate a String ID from the title of this section.
65
55
  #
66
- # If the generated id is already in use in the document, a count is appended
67
- # until a unique id is found.
68
- #
69
- # Examples
70
- #
71
- # section = Section.new(parent)
72
- # section.title = "Foo"
73
- # section.generate_id
74
- # => "_foo"
75
- #
76
- # another_section = Section.new(parent)
77
- # another_section.title = "Foo"
78
- # another_section.generate_id
79
- # => "_foo_1"
80
- #
81
- # yet_another_section = Section.new(parent)
82
- # yet_another_section.title = "Ben & Jerry"
83
- # yet_another_section.generate_id
84
- # => "_ben_jerry"
56
+ # See Section.generate_id for details.
85
57
  def generate_id
86
- if @document.attributes.has_key? 'sectids'
87
- sep = @document.attributes['idseparator'] || '_'
88
- pre = @document.attributes['idprefix'] || '_'
89
- base_id = %(#{pre}#{title.downcase.gsub(InvalidSectionIdCharsRx, sep).tr_s(sep, sep).chomp(sep)})
90
- # ensure id doesn't begin with idseparator if idprefix is empty and idseparator is not empty
91
- if pre.empty? && !sep.empty? && base_id.start_with?(sep)
92
- base_id = base_id[1..-1]
93
- base_id = base_id[1..-1] while base_id.start_with?(sep)
94
- end
95
- gen_id = base_id
96
- cnt = Compliance.unique_id_start_index
97
- while @document.references[:ids].has_key? gen_id
98
- gen_id = %(#{base_id}#{sep}#{cnt})
99
- cnt += 1
100
- end
101
- gen_id
102
- else
103
- nil
104
- end
58
+ Section.generate_id title, @document
105
59
  end
106
60
 
107
61
  # Public: Get the section number for the current Section
@@ -156,6 +110,41 @@ class Section < AbstractBlock
156
110
  end
157
111
  end
158
112
 
113
+ # (see AbstractBlock#xreftext)
114
+ def xreftext xrefstyle = nil
115
+ if (val = reftext) && !val.empty?
116
+ val
117
+ elsif xrefstyle
118
+ if @numbered
119
+ case xrefstyle
120
+ when 'full'
121
+ if (type = @sectname) == 'chapter' || type == 'appendix'
122
+ quoted_title = sprintf sub_quotes('_%s_'), title
123
+ else
124
+ quoted_title = sprintf sub_quotes(@document.compat_mode ? %q(``%s'') : '"`%s`"'), title
125
+ end
126
+ if (signifier = @document.attributes[%(#{type}-refsig)])
127
+ %(#{signifier} #{sectnum '.', ','} #{quoted_title})
128
+ else
129
+ %(#{sectnum '.', ','} #{quoted_title})
130
+ end
131
+ when 'short'
132
+ if (signifier = @document.attributes[%(#{@sectname}-refsig)])
133
+ %(#{signifier} #{sectnum '.', ''})
134
+ else
135
+ sectnum '.', ''
136
+ end
137
+ else # 'basic'
138
+ (type = @sectname) == 'chapter' || type == 'appendix' ? (sprintf sub_quotes('_%s_'), title) : title
139
+ end
140
+ else # apply basic styling
141
+ (type = @sectname) == 'chapter' || type == 'appendix' ? (sprintf sub_quotes('_%s_'), title) : title
142
+ end
143
+ else
144
+ title
145
+ end
146
+ end
147
+
159
148
  # Public: Append a content block to this block's list of blocks.
160
149
  #
161
150
  # If the child block is a Section, assign an index to it.
@@ -164,17 +153,56 @@ class Section < AbstractBlock
164
153
  #
165
154
  # Returns The parent Block
166
155
  def << block
167
- assign_index block if block.context == :section
156
+ enumerate_section block if block.context == :section
168
157
  super
169
158
  end
170
159
 
171
160
  def to_s
172
- if @title != nil
173
- qualified_title = @numbered ? %(#{sectnum} #{@title}) : @title
174
- %(#<#{self.class}@#{object_id} {level: #{@level}, title: #{qualified_title.inspect}, blocks: #{@blocks.size}}>)
161
+ if @title
162
+ formal_title = @numbered ? %(#{sectnum} #{@title}) : @title
163
+ %(#<#{self.class}@#{object_id} {level: #{@level}, title: #{formal_title.inspect}, blocks: #{@blocks.size}}>)
175
164
  else
176
165
  super
177
166
  end
178
167
  end
168
+
169
+ # Public: Generate a String ID from the given section title.
170
+ #
171
+ # The generated ID is prefixed with value of the 'idprefix' attribute, which
172
+ # is an underscore by default. Invalid characters are replaced with the
173
+ # value of the 'idseparator' attribute, which is an underscore by default.
174
+ #
175
+ # If the generated ID is already in use in the document, a count is appended
176
+ # until a unique id is found.
177
+ #
178
+ # Section ID generation can be disabled by undefining the 'sectids' attribute.
179
+ #
180
+ # Examples
181
+ #
182
+ # Section.generate_id 'Foo', document
183
+ # => "_foo"
184
+ #
185
+ def self.generate_id title, document
186
+ attrs = document.attributes
187
+ sep = attrs['idseparator'] || '_'
188
+ pre = attrs['idprefix'] || '_'
189
+ gen_id = %(#{pre}#{title.downcase.gsub InvalidSectionIdCharsRx, sep})
190
+ unless sep.empty?
191
+ # remove repeat and trailing separator characters
192
+ gen_id = gen_id.tr_s sep, sep
193
+ gen_id = gen_id.chop if gen_id.end_with? sep
194
+ # ensure id doesn't begin with idseparator if idprefix is empty and idseparator is not empty
195
+ if pre.empty?
196
+ gen_id = gen_id.slice 1, gen_id.length while gen_id.start_with? sep
197
+ end
198
+ end
199
+ if document.catalog[:ids].key? gen_id
200
+ ids, cnt = document.catalog[:ids], Compliance.unique_id_start_index
201
+ cnt += 1 while ids.key?(candidate_id = %(#{gen_id}#{sep}#{cnt}))
202
+ candidate_id
203
+ else
204
+ gen_id
205
+ end
206
+ end
179
207
  end
180
208
  end
@@ -32,8 +32,8 @@ class Stylesheets
32
32
  </style>)
33
33
  end
34
34
 
35
- def write_primary_stylesheet target_dir
36
- ::File.open(::File.join(target_dir, primary_stylesheet_name), 'w') {|f| f.write primary_stylesheet_data }
35
+ def write_primary_stylesheet target_dir = '.'
36
+ ::IO.write(::File.join(target_dir, primary_stylesheet_name), primary_stylesheet_data)
37
37
  end
38
38
 
39
39
  def coderay_stylesheet_name
@@ -57,8 +57,8 @@ class Stylesheets
57
57
  </style>)
58
58
  end
59
59
 
60
- def write_coderay_stylesheet target_dir
61
- ::File.open(::File.join(target_dir, coderay_stylesheet_name), 'w') {|f| f.write coderay_stylesheet_data }
60
+ def write_coderay_stylesheet target_dir = '.'
61
+ ::IO.write(::File.join(target_dir, coderay_stylesheet_name), coderay_stylesheet_data)
62
62
  end
63
63
 
64
64
  def pygments_stylesheet_name style = nil
@@ -84,8 +84,8 @@ class Stylesheets
84
84
  </style>)
85
85
  end
86
86
 
87
- def write_pygments_stylesheet target_dir, style = nil
88
- ::File.open(::File.join(target_dir, pygments_stylesheet_name(style)), 'w') {|f| f.write pygments_stylesheet_data(style) }
87
+ def write_pygments_stylesheet target_dir = '.', style = nil
88
+ ::IO.write(::File.join(target_dir, pygments_stylesheet_name(style)), pygments_stylesheet_data(style))
89
89
  end
90
90
 
91
91
  #def load_coderay
@@ -4,34 +4,28 @@ module Asciidoctor
4
4
  # is intented to be mixed-in to Section and Block to provide operations for performing
5
5
  # the necessary substitutions.
6
6
  module Substitutors
7
-
8
- SPECIAL_CHARS = {
9
- '&' => '&amp;',
10
- '<' => '&lt;',
11
- '>' => '&gt;'
12
- }
13
-
14
- SPECIAL_CHARS_PATTERN = /[#{SPECIAL_CHARS.keys.join}]/
15
-
16
- SUBS = {
17
- :basic => [:specialcharacters],
18
- :normal => [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements],
19
- :verbatim => [:specialcharacters, :callouts],
20
- :title => [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements],
21
- :header => [:specialcharacters, :attributes],
22
- # by default, AsciiDoc performs :attributes and :macros on a pass block
23
- # TODO make this a compliance setting
24
- :pass => []
7
+ SpecialCharsRx = /[<&>]/
8
+ SpecialCharsTr = { '>' => '&gt;', '<' => '&lt;', '&' => '&amp;' }
9
+
10
+ # Detects if text is a possible candidate for the quotes substitution.
11
+ QuotedTextSniffRx = { false => /[*_`#^~]/, true => /[*'_+#^~]/ }
12
+
13
+ (BASIC_SUBS = [:specialcharacters]).freeze
14
+ (HEADER_SUBS = [:specialcharacters, :attributes]).freeze
15
+ (NORMAL_SUBS = [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements]).freeze
16
+ (NONE_SUBS = []).freeze
17
+ (TITLE_SUBS = [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements]).freeze
18
+ (REFTEXT_SUBS = [:specialcharacters, :quotes, :replacements]).freeze
19
+ (VERBATIM_SUBS = [:specialcharacters, :callouts]).freeze
20
+
21
+ SUB_GROUPS = {
22
+ :none => NONE_SUBS,
23
+ :normal => NORMAL_SUBS,
24
+ :verbatim => VERBATIM_SUBS,
25
+ :specialchars => BASIC_SUBS
25
26
  }
26
27
 
27
- COMPOSITE_SUBS = {
28
- :none => [],
29
- :normal => SUBS[:normal],
30
- :verbatim => SUBS[:verbatim],
31
- :specialchars => [:specialcharacters]
32
- }
33
-
34
- SUB_SYMBOLS = {
28
+ SUB_HINTS = {
35
29
  :a => :attributes,
36
30
  :m => :macros,
37
31
  :n => :normal,
@@ -43,8 +37,8 @@ module Substitutors
43
37
  }
44
38
 
45
39
  SUB_OPTIONS = {
46
- :block => COMPOSITE_SUBS.keys + SUBS[:normal] + [:callouts],
47
- :inline => COMPOSITE_SUBS.keys + SUBS[:normal]
40
+ :block => SUB_GROUPS.keys + NORMAL_SUBS + [:callouts],
41
+ :inline => SUB_GROUPS.keys + NORMAL_SUBS
48
42
  }
49
43
 
50
44
  SUB_HIGHLIGHT = ['coderay', 'pygments']
@@ -53,52 +47,63 @@ module Substitutors
53
47
  # See http://www.aivosto.com/vbtips/control-characters.html#listabout for characters to use
54
48
 
55
49
  # SPA, start of guarded protected area (\u0096)
56
- PASS_START = "\u0096"
50
+ PASS_START = %(\u0096)
57
51
 
58
52
  # EPA, end of guarded protected area (\u0097)
59
- PASS_END = "\u0097"
53
+ PASS_END = %(\u0097)
54
+
55
+ # match passthrough slot
56
+ PassSlotRx = /#{PASS_START}(\d+)#{PASS_END}/
57
+
58
+ # fix passthrough slot after syntax highlighting
59
+ HighlightedPassSlotRx = %r(<span\b[^>]*>#{PASS_START}</span>[^\d]*(\d+)[^\d]*<span\b[^>]*>#{PASS_END}</span>)
60
60
 
61
- # match placeholder record
62
- PASS_MATCH = /\u0096(\d+)\u0097/
61
+ RS = '\\'
63
62
 
64
- # fix placeholder record after syntax highlighting
65
- PASS_MATCH_HI = /<span[^>]*>\u0096<\/span>[^\d]*(\d+)[^\d]*<span[^>]*>\u0097<\/span>/
63
+ R_SB = ']'
64
+
65
+ ESC_R_SB = '\]'
66
+
67
+ PLUS = '+'
68
+
69
+ PygmentsWrapperDivRx = %r(<div class="pyhl">(.*)</div>)m
70
+ # NOTE handles all permutations of <pre> wrapper
71
+ # NOTE trailing whitespace appears when pygments-linenums-mode=table; <pre> has style attribute when pygments-css=inline
72
+ PygmentsWrapperPreRx = %r(<pre\b[^>]*?>(.*?)</pre>\s*)m
66
73
 
67
74
  # Internal: A String Array of passthough (unprocessed) text captured from this block
68
75
  attr_reader :passthroughs
69
76
 
70
- # Public: Apply the specified substitutions to the lines of text
77
+ # Public: Apply the specified substitutions to the source.
71
78
  #
72
- # source - The String or String Array of text to process
73
- # subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default: :normal)
74
- # expand - A Boolean to control whether sub aliases are expanded (default: true)
79
+ # source - The String or String Array of text to process; must not be nil.
80
+ # subs - The substitutions to perform; can be a Symbol or Symbol Array; must not be nil (default: :normal).
81
+ # expand - A Boolean to control whether substitution aliases are expanded (default: false).
75
82
  #
76
- # returns Either a String or String Array, whichever matches the type of the first argument
77
- def apply_subs source, subs = :normal, expand = false
78
- if !subs
83
+ # Returns a String or String Array with substitutions applied, matching the type of source argument.
84
+ def apply_subs source, subs = NORMAL_SUBS, expand = false
85
+ if source.empty? || subs.empty?
79
86
  return source
80
- elsif subs == :normal
81
- subs = SUBS[:normal]
82
87
  elsif expand
83
88
  if ::Symbol === subs
84
- subs = COMPOSITE_SUBS[subs] || [subs]
89
+ subs = SUB_GROUPS[subs] || [subs]
85
90
  else
86
91
  effective_subs = []
87
92
  subs.each do |key|
88
- if COMPOSITE_SUBS.has_key? key
89
- effective_subs += COMPOSITE_SUBS[key]
93
+ if (sub_group = SUB_GROUPS[key])
94
+ effective_subs += sub_group unless sub_group.empty?
90
95
  else
91
96
  effective_subs << key
92
97
  end
93
98
  end
94
99
 
95
- subs = effective_subs
100
+ if (subs = effective_subs).empty?
101
+ return source
102
+ end
96
103
  end
97
104
  end
98
105
 
99
- return source if subs.empty?
100
-
101
- text = (multiline = ::Array === source) ? source * EOL : source
106
+ text = (multiline = ::Array === source) ? source * LF : source
102
107
 
103
108
  if (has_passthroughs = subs.include? :macros)
104
109
  text = extract_passthroughs text
@@ -112,7 +117,7 @@ module Substitutors
112
117
  when :quotes
113
118
  text = sub_quotes text
114
119
  when :attributes
115
- text = sub_attributes(text.split EOL) * EOL
120
+ text = sub_attributes(text.split LF, -1) * LF if text.include? '{'
116
121
  when :replacements
117
122
  text = sub_replacements text
118
123
  when :macros
@@ -129,16 +134,18 @@ module Substitutors
129
134
  end
130
135
  text = restore_passthroughs text if has_passthroughs
131
136
 
132
- multiline ? (text.split EOL) : text
137
+ multiline ? (text.split LF, -1) : text
133
138
  end
134
139
 
135
140
  # Public: Apply normal substitutions.
136
141
  #
137
- # lines - The lines of text to process. Can be a String or a String Array
142
+ # An alias for apply_subs with default remaining arguments.
143
+ #
144
+ # text - The String text to which to apply normal substitutions
138
145
  #
139
- # returns - A String with normal substitutions performed
140
- def apply_normal_subs(lines)
141
- apply_subs(::Array === lines ? lines * EOL : lines)
146
+ # Returns the String with normal substitutions applied.
147
+ def apply_normal_subs text
148
+ apply_subs text
142
149
  end
143
150
 
144
151
  # Public: Apply substitutions for titles.
@@ -147,7 +154,16 @@ module Substitutors
147
154
  #
148
155
  # returns - A String with title substitutions performed
149
156
  def apply_title_subs(title)
150
- apply_subs title, SUBS[:title]
157
+ apply_subs title, TITLE_SUBS
158
+ end
159
+
160
+ # Public: Apply substitutions for reftext.
161
+ #
162
+ # text - The String to process
163
+ #
164
+ # Returns a String with all substitutions from the reftext substitution group applied
165
+ def apply_reftext_subs text
166
+ apply_subs text, REFTEXT_SUBS
151
167
  end
152
168
 
153
169
  # Public: Apply substitutions for header metadata and attribute assignments
@@ -156,7 +172,7 @@ module Substitutors
156
172
  #
157
173
  # returns - A String with header substitutions performed
158
174
  def apply_header_subs(text)
159
- apply_subs text, SUBS[:header]
175
+ apply_subs text, HEADER_SUBS
160
176
  end
161
177
 
162
178
  # Internal: Extract the passthrough text from the document for reinsertion after processing.
@@ -166,19 +182,12 @@ module Substitutors
166
182
  # returns - The text with the passthrough region substituted with placeholders
167
183
  def extract_passthroughs(text)
168
184
  compat_mode = @document.compat_mode
169
- text = text.gsub(PassInlineMacroRx) {
185
+ text = text.gsub(InlinePassMacroRx) {
170
186
  # alias match for Ruby 1.8.7 compat
171
187
  m = $~
172
188
  preceding = nil
173
189
 
174
- if (boundary = m[4]).nil_or_empty? # pass:[]
175
- if m[6] == '\\'
176
- # NOTE we don't look for nested pass:[] macros
177
- next m[0][1..-1]
178
- end
179
-
180
- @passthroughs[pass_key = @passthroughs.size] = {:text => (unescape_brackets m[8]), :subs => (m[7].nil_or_empty? ? [] : (resolve_pass_subs m[7]))}
181
- else # $$, ++ or +++
190
+ if (boundary = m[4]) # $$, ++, or +++
182
191
  # skip ++ in compat mode, handled as normal quoted text
183
192
  if compat_mode && boundary == '++'
184
193
  next m[2].nil_or_empty? ?
@@ -187,22 +196,15 @@ module Substitutors
187
196
  end
188
197
 
189
198
  attributes = m[2]
190
-
191
- # fix non-matching group results in Opal under Firefox
192
- if ::RUBY_ENGINE_OPAL
193
- attributes = nil if attributes == ''
194
- end
195
-
196
- escape_count = m[3].size
199
+ escape_count = m[3].length
197
200
  content = m[5]
198
201
  old_behavior = false
199
202
 
200
203
  if attributes
201
204
  if escape_count > 0
202
205
  # NOTE we don't look for nested unconstrained pass macros
203
- # must enclose string following next in " for Opal
204
- next "#{m[1]}[#{attributes}]#{'\\' * (escape_count - 1)}#{boundary}#{m[5]}#{boundary})"
205
- elsif m[1] == '\\'
206
+ next %(#{m[1]}[#{attributes}]#{RS * (escape_count - 1)}#{boundary}#{m[5]}#{boundary})
207
+ elsif m[1] == RS
206
208
  preceding = %([#{attributes}])
207
209
  attributes = nil
208
210
  else
@@ -214,21 +216,27 @@ module Substitutors
214
216
  end
215
217
  elsif escape_count > 0
216
218
  # NOTE we don't look for nested unconstrained pass macros
217
- # must enclose string following next in " for Opal
218
- next "#{m[1]}[#{attributes}]#{'\\' * (escape_count - 1)}#{boundary}#{m[5]}#{boundary}"
219
+ next %(#{RS * (escape_count - 1)}#{boundary}#{m[5]}#{boundary})
219
220
  end
220
- subs = (boundary == '+++' ? [] : [:specialcharacters])
221
+ subs = (boundary == '+++' ? [] : BASIC_SUBS)
221
222
 
222
223
  pass_key = @passthroughs.size
223
224
  if attributes
224
225
  if old_behavior
225
- @passthroughs[pass_key] = {:text => content, :subs => SUBS[:normal], :type => :monospaced, :attributes => attributes}
226
+ @passthroughs[pass_key] = {:text => content, :subs => NORMAL_SUBS, :type => :monospaced, :attributes => attributes}
226
227
  else
227
228
  @passthroughs[pass_key] = {:text => content, :subs => subs, :type => :unquoted, :attributes => attributes}
228
229
  end
229
230
  else
230
231
  @passthroughs[pass_key] = {:text => content, :subs => subs}
231
232
  end
233
+ else # pass:[]
234
+ if m[6] == RS
235
+ # NOTE we don't look for nested pass:[] macros
236
+ next m[0][1..-1]
237
+ end
238
+
239
+ @passthroughs[pass_key = @passthroughs.size] = {:text => (unescape_brackets m[8]), :subs => (m[7] ? (resolve_pass_subs m[7]) : [])}
232
240
  end
233
241
 
234
242
  %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END})
@@ -240,15 +248,10 @@ module Substitutors
240
248
  m = $~
241
249
  preceding = m[1]
242
250
  attributes = m[2]
243
- escape_mark = (m[3].start_with? '\\') ? '\\' : nil
251
+ escape_mark = RS if m[3].start_with? RS
244
252
  format_mark = m[4]
245
253
  content = m[5]
246
254
 
247
- # fix non-matching group results in Opal under Firefox
248
- if ::RUBY_ENGINE_OPAL
249
- attributes = nil if attributes == ''
250
- end
251
-
252
255
  if compat_mode
253
256
  old_behavior = true
254
257
  else
@@ -259,14 +262,13 @@ module Substitutors
259
262
 
260
263
  if attributes
261
264
  if format_mark == '`' && !old_behavior
262
- # must enclose string following next in " for Opal
263
- next "#{preceding}[#{attributes}]#{escape_mark}`#{extract_passthroughs content}`"
265
+ next %(#{preceding}[#{attributes}]#{escape_mark}`#{extract_passthroughs content}`)
264
266
  end
265
267
 
266
268
  if escape_mark
267
- # honor the escape of the formatting mark (must enclose string following next in " for Opal)
268
- next "#{preceding}[#{attributes}]#{m[3][1..-1]}"
269
- elsif preceding == '\\'
269
+ # honor the escape of the formatting mark
270
+ next %(#{preceding}[#{attributes}]#{m[3][1..-1]})
271
+ elsif preceding == RS
270
272
  # honor the escape of the attributes
271
273
  preceding = %([#{attributes}])
272
274
  attributes = nil
@@ -274,49 +276,43 @@ module Substitutors
274
276
  attributes = parse_attributes attributes
275
277
  end
276
278
  elsif format_mark == '`' && !old_behavior
277
- # must enclose string following next in " for Opal
278
- next "#{preceding}#{escape_mark}`#{extract_passthroughs content}`"
279
+ next %(#{preceding}#{escape_mark}`#{extract_passthroughs content}`)
279
280
  elsif escape_mark
280
- # honor the escape of the formatting mark (must enclose string following next in " for Opal)
281
- next "#{preceding}#{m[3][1..-1]}"
281
+ # honor the escape of the formatting mark
282
+ next %(#{preceding}#{m[3][1..-1]})
282
283
  end
283
284
 
284
285
  pass_key = @passthroughs.size
285
286
  if compat_mode
286
- @passthroughs[pass_key] = {:text => content, :subs => [:specialcharacters], :attributes => attributes, :type => :monospaced}
287
+ @passthroughs[pass_key] = {:text => content, :subs => BASIC_SUBS, :attributes => attributes, :type => :monospaced}
287
288
  elsif attributes
288
289
  if old_behavior
289
- subs = (format_mark == '`' ? [:specialcharacters] : SUBS[:normal])
290
+ subs = (format_mark == '`' ? BASIC_SUBS : NORMAL_SUBS)
290
291
  @passthroughs[pass_key] = {:text => content, :subs => subs, :attributes => attributes, :type => :monospaced}
291
292
  else
292
- @passthroughs[pass_key] = {:text => content, :subs => [:specialcharacters], :attributes => attributes, :type => :unquoted}
293
+ @passthroughs[pass_key] = {:text => content, :subs => BASIC_SUBS, :attributes => attributes, :type => :unquoted}
293
294
  end
294
295
  else
295
- @passthroughs[pass_key] = {:text => content, :subs => [:specialcharacters]}
296
+ @passthroughs[pass_key] = {:text => content, :subs => BASIC_SUBS}
296
297
  end
297
298
 
298
299
  %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END})
299
300
  } if (text.include? pass_inline_char1) || (pass_inline_char2 && (text.include? pass_inline_char2))
300
301
 
301
302
  # NOTE we need to do the stem in a subsequent step to allow it to be escaped by the former
302
- text = text.gsub(StemInlineMacroRx) {
303
+ text = text.gsub(InlineStemMacroRx) {
303
304
  # alias match for Ruby 1.8.7 compat
304
305
  m = $~
305
306
  # honor the escape
306
- if m[0].start_with? '\\'
307
+ if m[0].start_with? RS
307
308
  next m[0][1..-1]
308
309
  end
309
310
 
310
311
  if (type = m[1].to_sym) == :stem
311
- type = ((default_stem_type = document.attributes['stem']).nil_or_empty? ? 'asciimath' : default_stem_type).to_sym
312
+ type = ((default_stem_type = @document.attributes['stem']).nil_or_empty? ? 'asciimath' : default_stem_type).to_sym
312
313
  end
313
314
  content = unescape_brackets m[3]
314
- if m[2].nil_or_empty?
315
- subs = (@document.basebackend? 'html') ? [:specialcharacters] : []
316
- else
317
- subs = resolve_pass_subs m[2]
318
- end
319
-
315
+ subs = m[2] ? (resolve_pass_subs m[2]) : ((@document.basebackend? 'html') ? BASIC_SUBS : [])
320
316
  @passthroughs[pass_key = @passthroughs.size] = {:text => content, :subs => subs, :type => type}
321
317
  %(#{PASS_START}#{pass_key}#{PASS_END})
322
318
  } if (text.include? ':') && ((text.include? 'stem:') || (text.include? 'math:'))
@@ -335,10 +331,10 @@ module Substitutors
335
331
  return text
336
332
  end
337
333
 
338
- text.gsub(PASS_MATCH) {
334
+ text.gsub(PassSlotRx) {
339
335
  # NOTE we can't remove entry from map because placeholder may have been duplicated by other substitutions
340
- pass = @passthroughs[$~[1].to_i]
341
- subbed_text = (subs = pass[:subs]) ? apply_subs(pass[:text], subs) : pass[:text]
336
+ pass = @passthroughs[$1.to_i]
337
+ subbed_text = apply_subs(pass[:text], pass[:subs])
342
338
  if (type = pass[:type])
343
339
  subbed_text = Inline.new(self, :quoted, subbed_text, :type => type, :attributes => pass[:attributes]).convert
344
340
  end
@@ -349,34 +345,29 @@ module Substitutors
349
345
  @passthroughs.clear if outer
350
346
  end
351
347
 
352
- # Public: Substitute special characters (i.e., encode XML)
353
- #
354
- # Special characters are defined in the Asciidoctor::SPECIAL_CHARS Array constant
355
- #
356
- # text - The String text to process
357
- #
358
- # returns The String text with special characters replaced
359
- def sub_specialchars(text)
360
- SUPPORTS_GSUB_RESULT_HASH ?
361
- text.gsub(SPECIAL_CHARS_PATTERN, SPECIAL_CHARS) :
362
- text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] }
363
- end
364
- alias :sub_specialcharacters :sub_specialchars
365
348
 
366
349
  if RUBY_ENGINE == 'opal'
367
350
  def sub_quotes text
368
- QUOTE_SUBS[@document.compat_mode].each do |type, scope, pattern|
369
- text = text.gsub(pattern) { convert_quoted_text $~, type, scope }
351
+ if QuotedTextSniffRx[compat = @document.compat_mode].match? text
352
+ QUOTE_SUBS[compat].each do |type, scope, pattern|
353
+ text = text.gsub(pattern) { convert_quoted_text $~, type, scope }
354
+ end
370
355
  end
371
356
  text
372
357
  end
373
358
 
374
359
  def sub_replacements text
375
- REPLACEMENTS.each do |pattern, replacement, restore|
376
- text = text.gsub(pattern) { do_replacement $~, replacement, restore }
360
+ if ReplaceableTextRx.match? text
361
+ REPLACEMENTS.each do |pattern, replacement, restore|
362
+ text = text.gsub(pattern) { do_replacement $~, replacement, restore }
363
+ end
377
364
  end
378
365
  text
379
366
  end
367
+
368
+ def sub_specialchars text
369
+ (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub SpecialCharsRx, SpecialCharsTr) : text
370
+ end
380
371
  else
381
372
  # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc)
382
373
  #
@@ -384,11 +375,13 @@ module Substitutors
384
375
  #
385
376
  # returns The converted String text
386
377
  def sub_quotes text
387
- # NOTE interpolation is faster than String#dup
388
- text = %(#{text})
389
- # NOTE using gsub! here as an MRI Ruby optimization
390
- QUOTE_SUBS[@document.compat_mode].each do |type, scope, pattern|
391
- text.gsub!(pattern) { convert_quoted_text $~, type, scope }
378
+ if QuotedTextSniffRx[compat = @document.compat_mode].match? text
379
+ # NOTE interpolation is faster than String#dup
380
+ text = %(#{text})
381
+ QUOTE_SUBS[compat].each do |type, scope, pattern|
382
+ # NOTE using gsub! here as an MRI Ruby optimization
383
+ text.gsub!(pattern) { convert_quoted_text $~, type, scope }
384
+ end
392
385
  end
393
386
  text
394
387
  end
@@ -399,30 +392,52 @@ module Substitutors
399
392
  #
400
393
  # returns The String text with the replacement characters substituted
401
394
  def sub_replacements text
402
- # NOTE interpolation is faster than String#dup
403
- text = %(#{text})
404
- # NOTE Using gsub! as optimization
405
- REPLACEMENTS.each do |pattern, replacement, restore|
406
- text.gsub!(pattern) { do_replacement $~, replacement, restore }
395
+ if ReplaceableTextRx.match? text
396
+ # NOTE interpolation is faster than String#dup
397
+ text = %(#{text})
398
+ REPLACEMENTS.each do |pattern, replacement, restore|
399
+ # NOTE Using gsub! as optimization
400
+ text.gsub!(pattern) { do_replacement $~, replacement, restore }
401
+ end
407
402
  end
408
403
  text
409
404
  end
405
+
406
+ # Public: Substitute special characters (i.e., encode XML)
407
+ #
408
+ # The special characters are <, &, and >, which get replaced with &lt;,
409
+ # &amp;, and &gt;, respectively.
410
+ #
411
+ # text - The String text to process
412
+ #
413
+ # returns The String text with special characters replaced
414
+ if ::RUBY_MIN_VERSION_1_9
415
+ def sub_specialchars text
416
+ (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub! SpecialCharsRx, SpecialCharsTr) : text
417
+ end
418
+ else
419
+ def sub_specialchars text
420
+ (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub!(SpecialCharsRx) { SpecialCharsTr[$&] }) : text
421
+ end
422
+ end
410
423
  end
424
+ alias sub_specialcharacters sub_specialchars
411
425
 
412
426
  # Internal: Substitute replacement text for matched location
413
427
  #
414
428
  # returns The String text with the replacement characters substituted
415
429
  def do_replacement m, replacement, restore
416
- if (matched = m[0]).include? '\\'
417
- matched.tr '\\', ''
430
+ if (captured = m[0]).include? RS
431
+ # we have to use sub since we aren't sure it's the first char
432
+ captured.sub RS, ''
418
433
  else
419
434
  case restore
420
435
  when :none
421
436
  replacement
422
- when :leading
423
- %(#{m[1]}#{replacement})
424
437
  when :bounding
425
438
  %(#{m[1]}#{replacement}#{m[2]})
439
+ else # :leading
440
+ %(#{m[1]}#{replacement})
426
441
  end
427
442
  end
428
443
  end
@@ -441,74 +456,52 @@ module Substitutors
441
456
  # so that a missing key doesn't wipe out the whole block of data
442
457
  # when attribute-undefined and/or attribute-missing is drop-line
443
458
  def sub_attributes data, opts = {}
444
- return data if data.nil_or_empty?
445
-
446
459
  # normalizes data type to an array (string becomes single-element array)
447
- if (string_data = ::String === data)
448
- data = [data]
449
- end
450
-
451
- doc_attrs = @document.attributes
452
- attribute_missing = nil
453
- result = []
460
+ data = [data] if (input_is_string = ::String === data)
461
+ doc_attrs, result = @document.attributes, []
454
462
  data.each do |line|
455
- reject = false
456
- reject_if_empty = false
463
+ reject = reject_if_empty = false
457
464
  line = line.gsub(AttributeReferenceRx) {
458
- # alias match for Ruby 1.8.7 compat
459
- m = $~
460
465
  # escaped attribute, return unescaped
461
- if m[1] == '\\' || m[4] == '\\'
462
- %({#{m[2]}})
463
- elsif !m[3].nil_or_empty?
464
- offset = (directive = m[3]).length + 1
465
- expr = m[2][offset..-1]
466
- case directive
466
+ if $1 == RS || $4 == RS
467
+ %({#{$2}})
468
+ elsif $3
469
+ case (args = $2.split ':', 3).shift
467
470
  when 'set'
468
- args = expr.split(':')
469
- _, value = Parser.store_attribute(args[0], args[1] || '', @document)
470
- unless value
471
- # since this is an assignment, only drop-line applies here (skip and drop imply the same result)
472
- if doc_attrs.fetch('attribute-undefined', Compliance.attribute_undefined) == 'drop-line'
473
- reject = true
474
- break ''
475
- end
476
- end
471
+ _, value = Parser.store_attribute args[0], args[1] || '', @document
472
+ # since this is an assignment, only drop-line applies here (skip and drop imply the same result)
473
+ if (doc_attrs.fetch 'attribute-undefined', Compliance.attribute_undefined) == 'drop-line'
474
+ reject = true
475
+ break ''
476
+ end unless value
477
477
  reject_if_empty = true
478
478
  ''
479
- when 'counter', 'counter2'
480
- args = expr.split(':')
481
- val = @document.counter(args[0], args[1])
482
- if directive == 'counter2'
483
- reject_if_empty = true
484
- ''
485
- else
486
- val
487
- end
488
- else
489
- # if we get here, our AttributeReference regex is too loose
490
- warn %(asciidoctor: WARNING: illegal attribute directive: #{m[3]})
491
- m[0]
479
+ when 'counter2'
480
+ @document.counter(*args)
481
+ reject_if_empty = true
482
+ ''
483
+ else # 'counter'
484
+ @document.counter(*args)
492
485
  end
493
- elsif doc_attrs.key?(key = m[2].downcase)
486
+ elsif doc_attrs.key?(key = $2.downcase)
494
487
  doc_attrs[key]
495
488
  elsif INTRINSIC_ATTRIBUTES.key? key
496
489
  INTRINSIC_ATTRIBUTES[key]
497
490
  else
498
- case (attribute_missing ||= (opts[:attribute_missing] || doc_attrs.fetch('attribute-missing', Compliance.attribute_missing)))
499
- when 'skip'
500
- m[0]
491
+ case (attribute_missing ||= opts[:attribute_missing] || (doc_attrs.fetch 'attribute-missing', Compliance.attribute_missing))
492
+ when 'drop'
493
+ # QUESTION should we warn in this case?
494
+ reject_if_empty = true
495
+ ''
501
496
  when 'drop-line'
502
497
  warn %(asciidoctor: WARNING: dropping line containing reference to missing attribute: #{key})
503
498
  reject = true
504
499
  break ''
505
500
  when 'warn'
506
501
  warn %(asciidoctor: WARNING: skipping reference to missing attribute: #{key})
507
- m[0]
508
- else # 'drop'
509
- # QUESTION should we warn in this case?
510
- reject_if_empty = true
511
- ''
502
+ $&
503
+ else # 'skip'
504
+ $&
512
505
  end
513
506
  end
514
507
  } if line.include? '{'
@@ -516,7 +509,7 @@ module Substitutors
516
509
  result << line unless reject || (reject_if_empty && line.empty?)
517
510
  end
518
511
 
519
- string_data ? result * EOL : result
512
+ input_is_string ? result * LF : result
520
513
  end
521
514
 
522
515
  # Public: Substitute inline macros (e.g., links, images, etc)
@@ -527,91 +520,80 @@ module Substitutors
527
520
  #
528
521
  # returns The converted String text
529
522
  def sub_macros(source)
530
- return source if source.nil_or_empty?
531
-
523
+ #return source if source.nil_or_empty?
532
524
  # some look ahead assertions to cut unnecessary regex calls
533
525
  found = {}
534
- found[:square_bracket] = source.include?('[')
535
- found[:round_bracket] = source.include?('(')
536
- found[:colon] = found_colon = source.include?(':')
537
- found[:macroish] = (found[:square_bracket] && found_colon)
538
- found[:macroish_short_form] = (found[:square_bracket] && found_colon && source.include?(':['))
539
- use_link_attrs = @document.attributes.has_key?('linkattrs')
540
- experimental = @document.attributes.has_key?('experimental')
541
-
542
- # NOTE interpolation is faster than String#dup
543
- result = %(#{source})
544
-
545
- if experimental
546
- if found[:macroish_short_form] && (result.include?('kbd:') || result.include?('btn:'))
547
- result = result.gsub(KbdBtnInlineMacroRx) {
548
- # alias match for Ruby 1.8.7 compat
549
- m = $~
550
- # honor the escape
551
- if (captured = m[0]).start_with? '\\'
552
- next captured[1..-1]
553
- end
554
-
555
- if captured.start_with?('kbd')
556
- keys = unescape_bracketed_text m[1]
526
+ found_square_bracket = found[:square_bracket] = (source.include? '[')
527
+ found_colon = source.include? ':'
528
+ found_macroish = found[:macroish] = found_square_bracket && found_colon
529
+ found_macroish_short = found_macroish && (source.include? ':[')
530
+ doc_attrs = @document.attributes
531
+ use_link_attrs = doc_attrs.key? 'linkattrs'
532
+ result = source
557
533
 
558
- if keys == '+'
559
- keys = ['+']
534
+ if doc_attrs.key? 'experimental'
535
+ if found_macroish_short && ((result.include? 'kbd:') || (result.include? 'btn:'))
536
+ result = result.gsub(InlineKbdBtnMacroRx) {
537
+ # honor the escape
538
+ if $1
539
+ $&.slice 1, $&.length
540
+ elsif $2 == 'kbd'
541
+ if (keys = $3.strip).include? R_SB
542
+ keys = keys.gsub ESC_R_SB, R_SB
543
+ end
544
+ if keys.length > 1 && (delim_idx = (delim_idx = keys.index ',', 1) ?
545
+ [delim_idx, (keys.index '+', 1)].compact.min : (keys.index '+', 1))
546
+ delim = keys.slice delim_idx, 1
547
+ # NOTE handle special case where keys ends with delimiter (e.g., Ctrl++ or Ctrl,,)
548
+ if keys.end_with? delim
549
+ keys = (keys.chop.split delim, -1).map {|key| key.strip }
550
+ keys[-1] = %(#{keys[-1]}#{delim})
551
+ else
552
+ keys = keys.split(delim).map {|key| key.strip }
553
+ end
560
554
  else
561
- # need to use closure to work around lack of negative lookbehind
562
- keys = keys.split(KbdDelimiterRx).inject([]) {|c, key|
563
- if key.end_with?('++')
564
- c << key[0..-3].strip
565
- c << '+'
566
- else
567
- c << key.strip
568
- end
569
- c
570
- }
555
+ keys = [keys]
571
556
  end
572
- Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).convert
573
- elsif captured.start_with?('btn')
574
- label = unescape_bracketed_text m[1]
575
- Inline.new(self, :button, label).convert
557
+ (Inline.new self, :kbd, nil, :attributes => { 'keys' => keys }).convert
558
+ else # $2 == 'btn'
559
+ (Inline.new self, :button, (unescape_bracketed_text $3)).convert
576
560
  end
577
561
  }
578
562
  end
579
563
 
580
- if found[:macroish] && result.include?('menu:')
581
- result = result.gsub(MenuInlineMacroRx) {
564
+ if found_macroish && (result.include? 'menu:')
565
+ result = result.gsub(InlineMenuMacroRx) {
582
566
  # alias match for Ruby 1.8.7 compat
583
567
  m = $~
584
568
  # honor the escape
585
- if (captured = m[0]).start_with? '\\'
569
+ if (captured = m[0]).start_with? RS
586
570
  next captured[1..-1]
587
571
  end
588
572
 
589
- menu = m[1]
590
- items = m[2]
573
+ menu, items = m[1], m[2]
591
574
 
592
- if !items
593
- submenus = []
594
- menuitem = nil
595
- else
575
+ if items
576
+ items = items.gsub ESC_R_SB, R_SB if items.include? R_SB
596
577
  if (delim = items.include?('&gt;') ? '&gt;' : (items.include?(',') ? ',' : nil))
597
578
  submenus = items.split(delim).map {|it| it.strip }
598
579
  menuitem = submenus.pop
599
580
  else
600
- submenus = []
601
- menuitem = items.rstrip
581
+ submenus, menuitem = [], items.rstrip
602
582
  end
583
+ else
584
+ submenus, menuitem = [], nil
603
585
  end
604
586
 
605
587
  Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).convert
606
588
  }
607
589
  end
608
590
 
609
- if result.include?('"') && result.include?('&gt;')
591
+ if (result.include? '"') && (result.include? '&gt;')
610
592
  result = result.gsub(MenuInlineRx) {
611
593
  # alias match for Ruby 1.8.7 compat
612
594
  m = $~
613
595
  # honor the escape
614
- if (captured = m[0]).start_with? '\\'
596
+ if (captured = m[0]).start_with? RS
615
597
  next captured[1..-1]
616
598
  end
617
599
 
@@ -626,114 +608,117 @@ module Substitutors
626
608
 
627
609
  # FIXME this location is somewhat arbitrary, probably need to be able to control ordering
628
610
  # TODO this handling needs some cleanup
629
- if (extensions = @document.extensions) && extensions.inline_macros? # && found[:macroish]
611
+ if (extensions = @document.extensions) && extensions.inline_macros? # && found_macroish
630
612
  extensions.inline_macros.each do |extension|
631
613
  result = result.gsub(extension.instance.regexp) {
632
614
  # alias match for Ruby 1.8.7 compat
633
615
  m = $~
634
616
  # honor the escape
635
- if m[0].start_with? '\\'
617
+ if m[0].start_with? RS
636
618
  next m[0][1..-1]
637
619
  end
638
620
 
639
- target = m[1]
640
- attributes = if extension.config[:format] == :short
641
- # TODO if content_model is :attributes, set target to nil and parse attributes
642
- # maybe if content_model is :text, we should put content into text attribute
643
- {}
621
+ if (m.names rescue []).empty?
622
+ target, content, extconf = m[1], m[2], extension.config
623
+ else
624
+ target, content, extconf = (m[:target] rescue nil), (m[:content] rescue nil), extension.config
625
+ end
626
+ attributes = (attributes = extconf[:default_attrs]) ? attributes.dup : {}
627
+ if content.nil_or_empty?
628
+ attributes['text'] = content if content && extconf[:content_model] != :attributes
644
629
  else
645
- if extension.config[:content_model] == :attributes
646
- parse_attributes m[2], (extension.config[:pos_attrs] || []), :sub_input => true, :unescape_input => true
630
+ content = unescape_bracketed_text content
631
+ if extconf[:content_model] == :attributes
632
+ # QUESTION should we store the text in the _text key?
633
+ # QUESTION why is the sub_result option false? why isn't the unescape_input option true?
634
+ parse_attributes content, extconf[:pos_attrs] || [], :sub_result => false, :into => attributes
647
635
  else
648
- { 'text' => (unescape_bracketed_text m[2]) }
636
+ attributes['text'] = content
649
637
  end
650
638
  end
651
- extension.process_method[self, target, attributes]
639
+ # NOTE use content if target is not set (short form only); deprecated - remove in 1.6.0
640
+ replacement = extension.process_method[self, target || content, attributes]
641
+ Inline === replacement ? replacement.convert : replacement
652
642
  }
653
643
  end
654
644
  end
655
645
 
656
- if found[:macroish] && (result.include?('image:') || result.include?('icon:'))
646
+ if found_macroish && ((result.include? 'image:') || (result.include? 'icon:'))
657
647
  # image:filename.png[Alt Text]
658
- result = result.gsub(ImageInlineMacroRx) {
648
+ result = result.gsub(InlineImageMacroRx) {
659
649
  # alias match for Ruby 1.8.7 compat
660
650
  m = $~
661
651
  # honor the escape
662
- if m[0].start_with? '\\'
663
- next m[0][1..-1]
652
+ if (captured = m[0]).start_with? RS
653
+ next captured[1..-1]
664
654
  end
665
655
 
666
- raw_attrs = unescape_bracketed_text m[2]
667
- if m[0].start_with? 'icon:'
668
- type = 'icon'
669
- posattrs = ['size']
656
+ if captured.start_with? 'icon:'
657
+ type, posattrs = 'icon', ['size']
670
658
  else
671
- type = 'image'
672
- posattrs = ['alt', 'width', 'height']
659
+ type, posattrs = 'image', ['alt', 'width', 'height']
673
660
  end
674
- target = sub_attributes(m[1])
675
- unless type == 'icon'
676
- @document.register(:images, target)
677
- end
678
- attrs = parse_attributes(raw_attrs, posattrs)
679
- attrs['alt'] ||= Helpers.basename(target, true).tr('_-', ' ')
661
+ target = m[1]
662
+ @document.register(:images, target) unless type == 'icon'
663
+ attrs = parse_attributes(m[2], posattrs, :unescape_input => true)
664
+ attrs['alt'] ||= (attrs['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
680
665
  Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).convert
681
666
  }
682
667
  end
683
668
 
684
- if found[:macroish_short_form] || found[:round_bracket]
685
- # indexterm:[Tigers,Big cats]
669
+ if ((result.include? '((') && (result.include? '))')) ||
670
+ (found_macroish_short && (result.include? 'indexterm'))
686
671
  # (((Tigers,Big cats)))
687
- # indexterm2:[Tigers]
672
+ # indexterm:[Tigers,Big cats]
688
673
  # ((Tigers))
689
- result = result.gsub(IndextermInlineMacroRx) {
674
+ # indexterm2:[Tigers]
675
+ result = result.gsub(InlineIndextermMacroRx) {
690
676
  # alias match for Ruby 1.8.7 compat
691
677
  m = $~
692
678
 
693
679
  # honor the escape
694
- if m[0].start_with? '\\'
680
+ if m[0].start_with? RS
695
681
  next m[0][1..-1]
696
682
  end
697
683
 
698
- # fix non-matching group results in Opal under Firefox
699
- if ::RUBY_ENGINE_OPAL
700
- m[1] = nil if m[1] == ''
701
- end
702
-
703
- num_brackets = 0
704
- text_in_brackets = nil
705
- unless (macro_name = m[1])
706
- text_in_brackets = m[3]
707
- if (text_in_brackets.start_with? '(') && (text_in_brackets.end_with? ')')
708
- text_in_brackets = text_in_brackets[1...-1]
709
- num_brackets = 3
710
- else
711
- num_brackets = 2
712
- end
713
- end
714
-
715
- # non-visible
716
- if macro_name == 'indexterm' || num_brackets == 3
717
- if !macro_name
718
- # (((Tigers,Big cats)))
719
- terms = split_simple_csv normalize_string(text_in_brackets)
720
- else
721
- # indexterm:[Tigers,Big cats]
722
- terms = split_simple_csv normalize_string(m[2], true)
723
- end
724
- @document.register(:indexterms, [*terms])
725
- Inline.new(self, :indexterm, nil, :attributes => {'terms' => terms}).convert
726
- # visible
684
+ case m[1]
685
+ when 'indexterm'
686
+ # indexterm:[Tigers,Big cats]
687
+ terms = split_simple_csv(normalize_string m[2], true)
688
+ @document.register :indexterms, terms
689
+ (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
690
+ when 'indexterm2'
691
+ # indexterm2:[Tigers]
692
+ term = normalize_string m[2], true
693
+ @document.register :indexterms, [term]
694
+ (Inline.new self, :indexterm, term, :type => :visible).convert
727
695
  else
728
- if !macro_name
696
+ text, visible, before, after = m[3], true, nil, nil
697
+ if text.start_with? '('
698
+ if text.end_with? ')'
699
+ text, visible = (text.slice 1, text.length - 2), false
700
+ else
701
+ text, before, after = (text.slice 1, text.length - 1), '(', ''
702
+ end
703
+ elsif text.end_with? ')'
704
+ if text.start_with? '('
705
+ text, visible = (text.slice 1, text.length - 2), false
706
+ else
707
+ text, before, after = (text.slice 0, text.length - 1), '', ')'
708
+ end
709
+ end
710
+ if visible
729
711
  # ((Tigers))
730
- text = normalize_string text_in_brackets
712
+ term = normalize_string text
713
+ @document.register :indexterms, [term]
714
+ result = (Inline.new self, :indexterm, term, :type => :visible).convert
731
715
  else
732
- # indexterm2:[Tigers]
733
- text = normalize_string m[2], true
716
+ # (((Tigers,Big cats)))
717
+ terms = split_simple_csv(normalize_string text)
718
+ @document.register :indexterms, terms
719
+ result = (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
734
720
  end
735
- @document.register(:indexterms, [text])
736
- Inline.new(self, :indexterm, text, :type => :visible).convert
721
+ before ? %(#{before}#{result}#{after}) : result
737
722
  end
738
723
  }
739
724
  end
@@ -744,71 +729,65 @@ module Substitutors
744
729
  # alias match for Ruby 1.8.7 compat
745
730
  m = $~
746
731
  # honor the escape
747
- if m[2].start_with? '\\'
748
- # must enclose string following next in " for Opal
749
- next "#{m[1]}#{m[2][1..-1]}#{m[3]}"
732
+ if m[2].start_with? RS
733
+ next %(#{m[1]}#{m[2][1..-1]}#{m[3]})
750
734
  end
751
- # fix non-matching group results in Opal under Firefox
752
- if ::RUBY_ENGINE_OPAL
753
- m[3] = nil if m[3] == ''
754
- end
755
- # not a valid macro syntax w/o trailing square brackets
756
- # we probably shouldn't even get here...our regex is doing too much
757
- if m[1] == 'link:' && !m[3]
758
- next m[0]
735
+ # NOTE if text is non-nil, then we've matched a formal macro (i.e., trailing square brackets)
736
+ prefix, target, text, suffix = m[1], m[2], (macro = m[3]) || '', ''
737
+ if prefix == 'link:'
738
+ if macro
739
+ prefix = ''
740
+ else
741
+ # invalid macro syntax (link: prefix w/o trailing square brackets)
742
+ # we probably shouldn't even get here...our regex is doing too much
743
+ next m[0]
744
+ end
759
745
  end
760
- prefix = (m[1] != 'link:' ? m[1] : '')
761
- target = m[2]
762
- suffix = ''
763
- unless m[3] || target !~ UriTerminator
764
- case $~[0]
746
+ unless macro || UriTerminatorRx !~ target
747
+ case $&
765
748
  when ')'
766
- # strip the trailing )
767
- target = target[0..-2]
749
+ # strip trailing )
750
+ target = target.chop
768
751
  suffix = ')'
769
752
  when ';'
770
- # strip the <> around the link
753
+ # strip <> around URI
771
754
  if prefix.start_with?('&lt;') && target.end_with?('&gt;')
772
755
  prefix = prefix[4..-1]
773
- target = target[0..-5]
774
- # strip the ); from the end of the link
775
- elsif target.end_with?(');')
776
- target = target[0..-3]
777
- suffix = ');'
756
+ target = target[0...-4]
778
757
  else
779
- target = target[0..-2]
780
- suffix = ';'
758
+ # strip trailing ;
759
+ # check for trailing );
760
+ if (target = target.chop).end_with?(')')
761
+ target = target.chop
762
+ suffix = ');'
763
+ else
764
+ suffix = ';'
765
+ end
781
766
  end
782
767
  when ':'
783
- # strip the ): from the end of the link
784
- if target.end_with?('):')
785
- target = target[0..-3]
768
+ # strip trailing :
769
+ # check for trailing ):
770
+ if (target = target.chop).end_with?(')')
771
+ target = target.chop
786
772
  suffix = '):'
787
773
  else
788
- target = target[0..-2]
789
774
  suffix = ':'
790
775
  end
791
776
  end
792
777
  end
793
- @document.register(:links, target)
794
778
 
795
- link_opts = { :type => :link, :target => target }
796
- attrs = nil
797
- #text = m[3] ? sub_attributes(m[3].gsub('\]', ']')) : ''
798
- if m[3].nil_or_empty?
799
- text = ''
800
- else
801
- if use_link_attrs && (m[3].start_with?('"') || (m[3].include?(',') && m[3].include?('=')))
802
- attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']')), [])
803
- link_opts[:id] = (attrs.delete 'id') if attrs.has_key? 'id'
779
+ attrs, link_opts = nil, { :type => :link }
780
+ unless text.empty?
781
+ text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
782
+ if use_link_attrs && ((text.start_with? '"') || ((text.include? ',') && (text.include? '=')))
783
+ attrs = parse_attributes text, []
784
+ link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
804
785
  text = attrs[1] || ''
805
- else
806
- text = sub_attributes(m[3].gsub('\]', ']'))
807
786
  end
808
787
 
809
- # TODO enable in Asciidoctor 1.5.1
788
+ # TODO enable in Asciidoctor 1.6.x
810
789
  # support pipe-separated text and title
811
- #unless attrs && (attrs.has_key? 'title')
790
+ #unless attrs && (attrs.key? 'title')
812
791
  # if text.include? '|'
813
792
  # attrs ||= {}
814
793
  # text, attrs['title'] = text.split '|', 2
@@ -820,120 +799,99 @@ module Substitutors
820
799
  if attrs
821
800
  attrs['window'] ||= '_blank'
822
801
  else
823
- attrs = {'window' => '_blank'}
802
+ attrs = { 'window' => '_blank' }
824
803
  end
825
804
  end
826
805
  end
827
806
 
828
807
  if text.empty?
829
- if @document.attr? 'hide-uri-scheme'
830
- text = target.sub UriSniffRx, ''
831
- else
832
- text = target
833
- end
834
-
808
+ text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
835
809
  if attrs
836
- attrs['role'] = %(bare #{attrs['role']}).chomp ' '
810
+ attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
837
811
  else
838
- attrs = {'role' => 'bare'}
812
+ attrs = { 'role' => 'bare' }
839
813
  end
840
814
  end
841
815
 
816
+ @document.register :links, (link_opts[:target] = target)
842
817
  link_opts[:attributes] = attrs if attrs
843
818
  %(#{prefix}#{Inline.new(self, :anchor, text, link_opts).convert}#{suffix})
844
819
  }
845
820
  end
846
821
 
847
- if found[:macroish] && (result.include? 'link:') || (result.include? 'mailto:')
822
+ if found_macroish && ((result.include? 'link:') || (result.include? 'mailto:'))
848
823
  # inline link macros, link:target[text]
849
- result = result.gsub(LinkInlineMacroRx) {
824
+ result = result.gsub(InlineLinkMacroRx) {
850
825
  # alias match for Ruby 1.8.7 compat
851
826
  m = $~
852
827
  # honor the escape
853
- if m[0].start_with? '\\'
828
+ if m[0].start_with? RS
854
829
  next m[0][1..-1]
855
830
  end
856
- raw_target = m[1]
857
- mailto = m[0].start_with?('mailto:')
858
- target = mailto ? %(mailto:#{raw_target}) : raw_target
859
-
860
- link_opts = { :type => :link, :target => target }
861
- attrs = nil
862
- #text = sub_attributes(m[2].gsub('\]', ']'))
863
- text = if use_link_attrs && (m[2].start_with?('"') || m[2].include?(','))
864
- attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']')), [])
865
- link_opts[:id] = (attrs.delete 'id') if attrs.key? 'id'
866
- if mailto
867
- if attrs.key? 2
868
- target = link_opts[:target] = "#{target}?subject=#{Helpers.encode_uri(attrs[2])}"
869
-
870
- if attrs.key? 3
871
- target = link_opts[:target] = "#{target}&amp;body=#{Helpers.encode_uri(attrs[3])}"
831
+ target = (mailto = m[1]) ? %(mailto:#{m[2]}) : m[2]
832
+ attrs, link_opts = nil, { :type => :link }
833
+ unless (text = m[3]).empty?
834
+ text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
835
+ if use_link_attrs && ((text.start_with? '"') || ((text.include? ',') && (mailto || (text.include? '='))))
836
+ attrs = parse_attributes text, []
837
+ link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
838
+ if mailto
839
+ if attrs.key? 2
840
+ if attrs.key? 3
841
+ target = %(#{target}?subject=#{Helpers.uri_encode attrs[2]}&amp;body=#{Helpers.uri_encode attrs[3]})
842
+ else
843
+ target = %(#{target}?subject=#{Helpers.uri_encode attrs[2]})
844
+ end
872
845
  end
873
846
  end
847
+ text = attrs[1] || ''
874
848
  end
875
- attrs[1]
876
- else
877
- sub_attributes(m[2].gsub('\]', ']'))
878
- end
879
849
 
880
- # QUESTION should a mailto be registered as an e-mail address?
881
- @document.register(:links, target)
850
+ # TODO enable in Asciidoctor 1.6.x
851
+ # support pipe-separated text and title
852
+ #unless attrs && (attrs.key? 'title')
853
+ # if text.include? '|'
854
+ # attrs ||= {}
855
+ # text, attrs['title'] = text.split '|', 2
856
+ # end
857
+ #end
882
858
 
883
- # TODO enable in Asciidoctor 1.5.1
884
- # support pipe-separated text and title
885
- #unless attrs && (attrs.key? 'title')
886
- # if text.include? '|'
887
- # attrs ||= {}
888
- # text, attrs['title'] = text.split '|', 2
889
- # end
890
- #end
891
-
892
- if text.end_with? '^'
893
- text = text.chop
894
- if attrs
895
- attrs['window'] ||= '_blank'
896
- else
897
- attrs = {'window' => '_blank'}
859
+ if text.end_with? '^'
860
+ text = text.chop
861
+ if attrs
862
+ attrs['window'] ||= '_blank'
863
+ else
864
+ attrs = { 'window' => '_blank' }
865
+ end
898
866
  end
899
867
  end
900
868
 
901
869
  if text.empty?
902
870
  # mailto is a special case, already processed
903
871
  if mailto
904
- text = raw_target
872
+ text = m[2]
905
873
  else
906
- if @document.attr? 'hide-uri-scheme'
907
- text = raw_target.sub UriSniffRx, ''
908
- else
909
- text = raw_target
910
- end
911
-
874
+ text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
912
875
  if attrs
913
- attrs['role'] = %(bare #{attrs['role']}).chomp ' '
876
+ attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
914
877
  else
915
- attrs = {'role' => 'bare'}
878
+ attrs = { 'role' => 'bare' }
916
879
  end
917
880
  end
918
881
  end
919
882
 
883
+ # QUESTION should a mailto be registered as an e-mail address?
884
+ @document.register :links, (link_opts[:target] = target)
920
885
  link_opts[:attributes] = attrs if attrs
921
886
  Inline.new(self, :anchor, text, link_opts).convert
922
887
  }
923
888
  end
924
889
 
925
890
  if result.include? '@'
926
- result = result.gsub(EmailInlineMacroRx) {
927
- # alias match for Ruby 1.8.7 compat
928
- m = $~
929
- address = m[0]
930
- if (lead = m[1])
931
- case lead
932
- when '\\'
933
- next address[1..-1]
934
- else
935
- next address
936
- end
891
+ result = result.gsub(EmailInlineRx) {
892
+ address, tip = $&, $1
893
+ if tip
894
+ next (tip == RS ? address[1..-1] : address)
937
895
  end
938
896
 
939
897
  target = %(mailto:#{address})
@@ -944,12 +902,12 @@ module Substitutors
944
902
  }
945
903
  end
946
904
 
947
- if found[:macroish_short_form] && result.include?('footnote')
948
- result = result.gsub(FootnoteInlineMacroRx) {
905
+ if found_macroish_short && (result.include? 'footnote')
906
+ result = result.gsub(InlineFootnoteMacroRx) {
949
907
  # alias match for Ruby 1.8.7 compat
950
908
  m = $~
951
909
  # honor the escape
952
- if m[0].start_with? '\\'
910
+ if m[0].start_with? RS
953
911
  next m[0][1..-1]
954
912
  end
955
913
  if m[1] == 'footnote'
@@ -963,9 +921,15 @@ module Substitutors
963
921
  else
964
922
  id, text = m[2].split(',', 2)
965
923
  id = id.strip
966
- # NOTE In Opal, text is set to empty string if comma is missing
967
- if text.nil_or_empty?
968
- if (footnote = @document.references[:footnotes].find {|fn| fn.id == id })
924
+ if text
925
+ # REVIEW it's a dirty job, but somebody's gotta do it
926
+ text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string text, true)), false)
927
+ index = @document.counter('footnote-number')
928
+ @document.register(:footnotes, Document::Footnote.new(index, id, text))
929
+ type = :ref
930
+ target = nil
931
+ else
932
+ if (footnote = @document.footnotes.find {|fn| fn.id == id })
969
933
  index = footnote.index
970
934
  text = footnote.text
971
935
  else
@@ -975,13 +939,6 @@ module Substitutors
975
939
  target = id
976
940
  id = nil
977
941
  type = :xref
978
- else
979
- # REVIEW it's a dirty job, but somebody's gotta do it
980
- text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string text, true)), false)
981
- index = @document.counter('footnote-number')
982
- @document.register(:footnotes, Document::Footnote.new(index, id, text))
983
- type = :ref
984
- target = nil
985
942
  end
986
943
  end
987
944
  Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).convert
@@ -993,44 +950,29 @@ module Substitutors
993
950
 
994
951
  # Internal: Substitute normal and bibliographic anchors
995
952
  def sub_inline_anchors(text, found = nil)
996
- if (!found || found[:square_bracket]) && text.include?('[[[')
997
- text = text.gsub(InlineBiblioAnchorRx) {
998
- # alias match for Ruby 1.8.7 compat
999
- m = $~
1000
- # honor the escape
1001
- if m[0].start_with? '\\'
1002
- next m[0][1..-1]
1003
- end
1004
- id = reftext = m[1]
1005
- Inline.new(self, :anchor, reftext, :type => :bibref, :target => id).convert
953
+ if @context == :list_item && @parent.style == 'bibliography'
954
+ text = text.sub(InlineBiblioAnchorRx) {
955
+ # NOTE target property on :bibref is deprecated
956
+ Inline.new(self, :anchor, %([#{$2 || $1}]), :type => :bibref, :id => $1, :target => $1).convert
1006
957
  }
1007
958
  end
1008
959
 
1009
960
  if ((!found || found[:square_bracket]) && text.include?('[[')) ||
1010
- ((!found || found[:macroish]) && text.include?('anchor:'))
961
+ ((!found || found[:macroish]) && text.include?('or:'))
1011
962
  text = text.gsub(InlineAnchorRx) {
1012
- # alias match for Ruby 1.8.7 compat
1013
- m = $~
1014
963
  # honor the escape
1015
- if m[0].start_with? '\\'
1016
- next m[0][1..-1]
1017
- end
1018
- # fix non-matching group results in Opal under Firefox
1019
- if ::RUBY_ENGINE_OPAL
1020
- m[1] = nil if m[1] == ''
1021
- m[2] = nil if m[2] == ''
1022
- m[4] = nil if m[4] == ''
964
+ next $&.slice 1, $&.length if $1
965
+ # NOTE reftext is only relevant for DocBook output; used as value of xreflabel attribute
966
+ if (id = $2)
967
+ reftext = $3
968
+ else
969
+ id = $4
970
+ if (reftext = $5) && (reftext.include? R_SB)
971
+ reftext = reftext.gsub ESC_R_SB, R_SB
972
+ end
1023
973
  end
1024
- id = m[1] || m[3]
1025
- reftext = m[2] || m[4] || %([#{id}])
1026
- # enable if we want to allow double quoted values
1027
- #id = id.sub(DoubleQuotedRx, '\2')
1028
- #if reftext
1029
- # reftext = reftext.sub(DoubleQuotedMultiRx, '\2')
1030
- #else
1031
- # reftext = "[#{id}]"
1032
- #end
1033
- Inline.new(self, :anchor, reftext, :type => :ref, :target => id).convert
974
+ # NOTE target property on :ref is deprecated
975
+ Inline.new(self, :anchor, reftext, :type => :ref, :id => id, :target => id).convert
1034
976
  }
1035
977
  end
1036
978
 
@@ -1039,65 +981,67 @@ module Substitutors
1039
981
 
1040
982
  # Internal: Substitute cross reference links
1041
983
  def sub_inline_xrefs(text, found = nil)
1042
- if (!found || found[:macroish]) || text.include?('&lt;&lt;')
1043
- text = text.gsub(XrefInlineMacroRx) {
984
+ if ((found ? found[:macroish] : (text.include? '[')) && (text.include? 'xref:')) ||
985
+ ((text.include? '&') && (text.include? '&lt;&lt;'))
986
+ text = text.gsub(InlineXrefMacroRx) {
1044
987
  # alias match for Ruby 1.8.7 compat
1045
988
  m = $~
1046
989
  # honor the escape
1047
- if m[0].start_with? '\\'
990
+ if m[0].start_with? RS
1048
991
  next m[0][1..-1]
1049
992
  end
1050
- # fix non-matching group results in Opal under Firefox
1051
- if ::RUBY_ENGINE_OPAL
1052
- m[1] = nil if m[1] == ''
1053
- end
1054
- if m[1]
1055
- id, reftext = m[1].split(',', 2).map {|it| it.strip }
1056
- id = id.sub(DoubleQuotedRx, '\2')
1057
- # NOTE In Opal, reftext is set to empty string if comma is missing
1058
- reftext = if reftext.nil_or_empty?
1059
- nil
1060
- else
1061
- reftext.sub(DoubleQuotedMultiRx, '\2')
1062
- end
993
+ if (id = m[1])
994
+ id, reftext = id.split ',', 2
995
+ reftext = reftext.lstrip if reftext
1063
996
  else
1064
997
  id = m[2]
1065
- reftext = m[3] unless m[3].nil_or_empty?
998
+ if (reftext = m[3]) && (reftext.include? R_SB)
999
+ reftext = reftext.gsub ESC_R_SB, R_SB
1000
+ end
1066
1001
  end
1067
1002
 
1068
- if id.include? '#'
1069
- path, fragment = id.split('#')
1070
- # QUESTION perform this check and throw it back if it fails?
1071
- #elsif (start_chr = id.chr) == '.' || start_chr == '/'
1072
- # next m[0][1..-1]
1003
+ if (hash_idx = id.index '#')
1004
+ if hash_idx > 0
1005
+ if (fragment_len = id.length - hash_idx - 1) > 0
1006
+ path, fragment = (id.slice 0, hash_idx), (id.slice hash_idx + 1, fragment_len)
1007
+ else
1008
+ path, fragment = (id.slice 0, hash_idx), nil
1009
+ end
1010
+ else
1011
+ target, path, fragment = id, nil, (id.slice 1, id.length)
1012
+ end
1073
1013
  else
1074
- path = nil
1075
- fragment = id
1014
+ path, fragment = nil, id
1076
1015
  end
1077
1016
 
1078
- # handles forms: doc#, doc.adoc#, doc#id and doc.adoc#id
1079
- if path
1080
- path = Helpers.rootname(path)
1017
+ # handles: #id
1018
+ if target
1019
+ refid = fragment
1020
+ # handles: path#, path.adoc#, path#id, or path.adoc#id
1021
+ elsif path
1022
+ if (ext_idx = path.rindex '.') && ASCIIDOC_EXTENSIONS[path.slice ext_idx, path.length]
1023
+ path = path.slice 0, ext_idx
1024
+ end
1081
1025
  # the referenced path is this document, or its contents has been included in this document
1082
- if @document.attributes['docname'] == path || @document.references[:includes].include?(path)
1083
- refid = fragment
1084
- path = nil
1085
- target = %(##{fragment})
1026
+ if @document.attributes['docname'] == path || @document.catalog[:includes].include?(path)
1027
+ refid, path, target = fragment, nil, %(##{fragment})
1086
1028
  else
1087
1029
  refid = fragment ? %(#{path}##{fragment}) : path
1088
- path = "#{@document.attributes['relfileprefix']}#{path}#{@document.attributes.fetch 'outfilesuffix', '.html'}"
1030
+ path = %(#{@document.attributes['relfileprefix']}#{path}#{@document.attributes.fetch 'outfilesuffix', '.html'})
1089
1031
  target = fragment ? %(#{path}##{fragment}) : path
1090
1032
  end
1091
- # handles form: id or Section Title
1033
+ # handles: id or Section Title
1092
1034
  else
1093
- # resolve fragment as reftext if cannot be resolved as refid and looks like reftext
1094
- if !(@document.references[:ids].has_key? fragment) &&
1095
- ((fragment.include? ' ') || fragment.downcase != fragment) &&
1096
- (resolved_id = RUBY_MIN_VERSION_1_9 ? (@document.references[:ids].key fragment) : (@document.references[:ids].index fragment))
1097
- fragment = resolved_id
1035
+ # resolve fragment as reftext if it's not a known ID and resembles reftext (includes space or has uppercase char)
1036
+ unless @document.catalog[:ids].key? fragment
1037
+ if ((fragment.include? ' ') || fragment.downcase != fragment) &&
1038
+ (resolved_id = @document.catalog[:ids].key fragment)
1039
+ fragment = resolved_id
1040
+ elsif $VERBOSE
1041
+ warn %(asciidoctor: WARNING: invalid reference: #{fragment})
1042
+ end
1098
1043
  end
1099
- refid = fragment
1100
- target = %(##{fragment})
1044
+ refid, target = fragment, %(##{fragment})
1101
1045
  end
1102
1046
  Inline.new(self, :anchor, reftext, :type => :xref, :target => target, :attributes => {'path' => path, 'fragment' => fragment, 'refid' => refid}).convert
1103
1047
  }
@@ -1115,14 +1059,11 @@ module Substitutors
1115
1059
  # FIXME cache this dynamic regex
1116
1060
  callout_rx = (attr? 'line-comment') ? /(?:#{::Regexp.escape(attr 'line-comment')} )?#{CalloutSourceRxt}/ : CalloutSourceRx
1117
1061
  text.gsub(callout_rx) {
1118
- # alias match for Ruby 1.8.7 compat
1119
- m = $~
1120
- # honor the escape
1121
- if m[1] == '\\'
1122
- # we have to do a sub since we aren't sure it's the first char
1123
- next m[0].sub('\\', '')
1062
+ if $1
1063
+ # we have to use sub since we aren't sure it's the first char
1064
+ next $&.sub(RS, '')
1124
1065
  end
1125
- Inline.new(self, :callout, m[3], :id => @document.callouts.read_next_id).convert
1066
+ Inline.new(self, :callout, $3, :id => @document.callouts.read_next_id).convert
1126
1067
  }
1127
1068
  end
1128
1069
 
@@ -1132,13 +1073,15 @@ module Substitutors
1132
1073
  #
1133
1074
  # Returns the converted String text
1134
1075
  def sub_post_replacements(text)
1135
- if (@document.attributes.has_key? 'hardbreaks') || (@attributes.has_key? 'hardbreaks-option')
1136
- lines = (text.split EOL)
1137
- return text if lines.size == 1
1076
+ if (@document.attributes.key? 'hardbreaks') || (@attributes.key? 'hardbreaks-option')
1077
+ lines = text.split LF, -1
1078
+ return text if lines.size < 2
1138
1079
  last = lines.pop
1139
- lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).convert }.push(last) * EOL
1140
- elsif text.include? '+'
1141
- text.gsub(LineBreakRx) { Inline.new(self, :break, $~[1], :type => :line).convert }
1080
+ (lines.map {|line|
1081
+ Inline.new(self, :break, (line.end_with? HARD_LINE_BREAK) ? (line.slice 0, line.length - 2) : line, :type => :line).convert
1082
+ } << last) * LF
1083
+ elsif (text.include? PLUS) && (text.include? HARD_LINE_BREAK)
1084
+ text.gsub(HardLineBreakRx) { Inline.new(self, :break, $1, :type => :line).convert }
1142
1085
  else
1143
1086
  text
1144
1087
  end
@@ -1152,8 +1095,7 @@ module Substitutors
1152
1095
  #
1153
1096
  # Returns The converted String text for the quoted text region
1154
1097
  def convert_quoted_text(match, type, scope)
1155
- unescaped_attrs = nil
1156
- if match[0].start_with? '\\'
1098
+ if match[0].start_with? RS
1157
1099
  if scope == :constrained && !(attrs = match[2]).nil_or_empty?
1158
1100
  unescaped_attrs = %([#{attrs}])
1159
1101
  else
@@ -1165,44 +1107,39 @@ module Substitutors
1165
1107
  if unescaped_attrs
1166
1108
  %(#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], :type => type).convert})
1167
1109
  else
1168
- if (attributes = parse_quoted_text_attributes(match[2]))
1169
- id = attributes.delete 'id'
1110
+ if (attrlist = match[2])
1111
+ id = (attributes = parse_quoted_text_attributes attrlist).delete 'id'
1170
1112
  type = :unquoted if type == :mark
1171
- else
1172
- id = nil
1173
1113
  end
1174
1114
  %(#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :id => id, :attributes => attributes).convert})
1175
1115
  end
1176
1116
  else
1177
- if (attributes = parse_quoted_text_attributes(match[1]))
1178
- id = attributes.delete 'id'
1117
+ if (attrlist = match[1])
1118
+ id = (attributes = parse_quoted_text_attributes attrlist).delete 'id'
1179
1119
  type = :unquoted if type == :mark
1180
- else
1181
- id = nil
1182
1120
  end
1183
1121
  Inline.new(self, :quoted, match[2], :type => type, :id => id, :attributes => attributes).convert
1184
1122
  end
1185
1123
  end
1186
1124
 
1187
- # Internal: Parse the attributes that are defined on quoted text
1125
+ # Internal: Parse the attributes that are defined on quoted (aka formatted) text
1188
1126
  #
1189
- # str - A String of unprocessed attributes (space-separated roles or the id/role shorthand syntax)
1127
+ # str - A non-nil String of unprocessed attributes;
1128
+ # space-separated roles (e.g., role1 role2) or the id/role shorthand syntax (e.g., #idname.role)
1190
1129
  #
1191
- # returns nil if str is nil, an empty Hash if str is empty, otherwise a Hash of attributes (role and id only)
1192
- def parse_quoted_text_attributes(str)
1193
- return unless str
1194
- return {} if str.empty?
1195
- str = sub_attributes(str) if str.include?('{')
1196
- str = str.strip
1130
+ # Returns a Hash of attributes (role and id only)
1131
+ def parse_quoted_text_attributes str
1132
+ # NOTE attributes are typically resolved after quoted text, so substitute eagerly
1133
+ str = sub_attributes str if str.include? '{'
1197
1134
  # for compliance, only consider first positional attribute
1198
- str, _ = str.split(',', 2) if str.include?(',')
1135
+ str = str.slice 0, (str.index ',') if str.include? ','
1199
1136
 
1200
- if str.empty?
1137
+ if (str = str.strip).empty?
1201
1138
  {}
1202
- elsif (str.start_with?('.') || str.start_with?('#')) && Compliance.shorthand_property_syntax
1139
+ elsif (str.start_with? '.', '#') && Compliance.shorthand_property_syntax
1203
1140
  segments = str.split('#', 2)
1204
1141
 
1205
- if segments.length > 1
1142
+ if segments.size > 1
1206
1143
  id, *more_roles = segments[1].split('.')
1207
1144
  else
1208
1145
  id = nil
@@ -1210,11 +1147,11 @@ module Substitutors
1210
1147
  end
1211
1148
 
1212
1149
  roles = segments[0].empty? ? [] : segments[0].split('.')
1213
- if roles.length > 1
1150
+ if roles.size > 1
1214
1151
  roles.shift
1215
1152
  end
1216
1153
 
1217
- if more_roles.length > 0
1154
+ if more_roles.size > 0
1218
1155
  roles.concat more_roles
1219
1156
  end
1220
1157
 
@@ -1236,14 +1173,10 @@ module Substitutors
1236
1173
  def parse_attributes(attrline, posattrs = ['role'], opts = {})
1237
1174
  return unless attrline
1238
1175
  return {} if attrline.empty?
1239
- attrline = @document.sub_attributes(attrline) if opts[:sub_input]
1176
+ attrline = @document.sub_attributes(attrline) if opts[:sub_input] && (attrline.include? '{')
1240
1177
  attrline = unescape_bracketed_text(attrline) if opts[:unescape_input]
1241
- block = nil
1242
- if opts.fetch(:sub_result, true)
1243
- # substitutions are only performed on attribute values if block is not nil
1244
- block = self
1245
- end
1246
-
1178
+ # substitutions are only performed on attribute values if block is not nil
1179
+ block = opts.fetch(:sub_result, true) ? self : nil
1247
1180
  if (into = opts[:into])
1248
1181
  AttributeList.new(attrline, block).parse_into(into, posattrs)
1249
1182
  else
@@ -1253,28 +1186,29 @@ module Substitutors
1253
1186
 
1254
1187
  # Internal: Strip bounding whitespace, fold endlines and unescaped closing
1255
1188
  # square brackets from text extracted from brackets
1256
- def unescape_bracketed_text(text)
1257
- return '' if text.empty?
1258
- # FIXME make \] a regex
1259
- text.strip.tr(EOL, ' ').gsub('\]', ']')
1189
+ def unescape_bracketed_text text
1190
+ if (text = text.strip.tr LF, ' ').include? R_SB
1191
+ text = text.gsub ESC_R_SB, R_SB
1192
+ end unless text.empty?
1193
+ text
1260
1194
  end
1261
1195
 
1262
1196
  # Internal: Strip bounding whitespace and fold endlines
1263
1197
  def normalize_string str, unescape_brackets = false
1264
- if str.empty?
1265
- ''
1266
- elsif unescape_brackets
1267
- unescape_brackets str.strip.tr(EOL, ' ')
1268
- else
1269
- str.strip.tr(EOL, ' ')
1198
+ unless str.empty?
1199
+ str = str.strip.tr LF, ' '
1200
+ str = str.gsub ESC_R_SB, R_SB if unescape_brackets && (str.include? R_SB)
1270
1201
  end
1202
+ str
1271
1203
  end
1272
1204
 
1273
1205
  # Internal: Unescape closing square brackets.
1274
1206
  # Intended for text extracted from square brackets.
1275
1207
  def unescape_brackets str
1276
- # FIXME make \] a regex
1277
- str.empty? ? '' : str.gsub('\]', ']')
1208
+ if str.include? RS
1209
+ str = str.gsub ESC_R_SB, R_SB
1210
+ end unless str.empty?
1211
+ str
1278
1212
  end
1279
1213
 
1280
1214
  # Internal: Split text formatted as CSV with support
@@ -1290,7 +1224,7 @@ module Substitutors
1290
1224
  case c
1291
1225
  when ','
1292
1226
  if quote_open
1293
- current.push c
1227
+ current << c
1294
1228
  else
1295
1229
  values << current.join.strip
1296
1230
  current = []
@@ -1298,7 +1232,7 @@ module Substitutors
1298
1232
  when '"'
1299
1233
  quote_open = !quote_open
1300
1234
  else
1301
- current.push c
1235
+ current << c
1302
1236
  end
1303
1237
  end
1304
1238
 
@@ -1317,9 +1251,11 @@ module Substitutors
1317
1251
  # returns An Array of Symbols representing the substitution operation
1318
1252
  def resolve_subs subs, type = :block, defaults = nil, subject = nil
1319
1253
  return [] if subs.nil_or_empty?
1254
+ # QUESTION should we store candidates as a Set instead of an Array?
1320
1255
  candidates = nil
1321
- modifiers_present = SubModifierSniffRx =~ subs
1322
- subs.tr(' ', '').split(',').each do |key|
1256
+ subs = subs.delete ' ' if subs.include? ' '
1257
+ modifiers_present = SubModifierSniffRx.match? subs
1258
+ subs.split(',').each do |key|
1323
1259
  modifier_operation = nil
1324
1260
  if modifiers_present
1325
1261
  if (first = key.chr) == '+'
@@ -1336,12 +1272,12 @@ module Substitutors
1336
1272
  key = key.to_sym
1337
1273
  # special case to disable callouts for inline subs
1338
1274
  if type == :inline && (key == :verbatim || key == :v)
1339
- resolved_keys = [:specialcharacters]
1340
- elsif COMPOSITE_SUBS.key? key
1341
- resolved_keys = COMPOSITE_SUBS[key]
1342
- elsif type == :inline && key.length == 1 && (SUB_SYMBOLS.key? key)
1343
- resolved_key = SUB_SYMBOLS[key]
1344
- if (candidate = COMPOSITE_SUBS[resolved_key])
1275
+ resolved_keys = BASIC_SUBS
1276
+ elsif SUB_GROUPS.key? key
1277
+ resolved_keys = SUB_GROUPS[key]
1278
+ elsif type == :inline && key.length == 1 && (SUB_HINTS.key? key)
1279
+ resolved_key = SUB_HINTS[key]
1280
+ if (candidate = SUB_GROUPS[resolved_key])
1345
1281
  resolved_keys = candidate
1346
1282
  else
1347
1283
  resolved_keys = [resolved_key]
@@ -1365,8 +1301,8 @@ module Substitutors
1365
1301
  candidates += resolved_keys
1366
1302
  end
1367
1303
  end
1368
- # weed out invalid options and remove duplicates (first wins)
1369
- # TODO may be use a set instead?
1304
+ return [] unless candidates
1305
+ # weed out invalid options and remove duplicates (order is preserved; first occurence wins)
1370
1306
  resolved = candidates & SUB_OPTIONS[type]
1371
1307
  unless (candidates - resolved).empty?
1372
1308
  invalid = candidates - resolved
@@ -1401,7 +1337,7 @@ module Substitutors
1401
1337
  unless (highlighter_loaded = defined? ::CodeRay) || @document.attributes['coderay-unavailable']
1402
1338
  if (Helpers.require_library 'coderay', true, :warn).nil?
1403
1339
  # prevent further attempts to load CodeRay
1404
- @document.set_attr 'coderay-unavailable', ''
1340
+ @document.set_attr 'coderay-unavailable'
1405
1341
  else
1406
1342
  highlighter_loaded = true
1407
1343
  end
@@ -1410,7 +1346,7 @@ module Substitutors
1410
1346
  unless (highlighter_loaded = defined? ::Pygments) || @document.attributes['pygments-unavailable']
1411
1347
  if (Helpers.require_library 'pygments', 'pygments.rb', :warn).nil?
1412
1348
  # prevent further attempts to load Pygments
1413
- @document.set_attr 'pygments-unavailable', ''
1349
+ @document.set_attr 'pygments-unavailable'
1414
1350
  else
1415
1351
  highlighter_loaded = true
1416
1352
  end
@@ -1430,21 +1366,22 @@ module Substitutors
1430
1366
  # FIXME cache this dynamic regex
1431
1367
  callout_rx = (attr? 'line-comment') ? /(?:#{::Regexp.escape(attr 'line-comment')} )?#{CalloutExtractRxt}/ : CalloutExtractRx
1432
1368
  # extract callout marks, indexed by line number
1433
- source = source.split(EOL).map {|line|
1369
+ source = source.split(LF, -1).map {|line|
1434
1370
  lineno = lineno + 1
1435
1371
  line.gsub(callout_rx) {
1436
1372
  # alias match for Ruby 1.8.7 compat
1437
1373
  m = $~
1438
1374
  # honor the escape
1439
- if m[1] == '\\'
1440
- m[0].sub('\\', '')
1375
+ if m[1] == RS
1376
+ # we have to use sub since we aren't sure it's the first char
1377
+ m[0].sub RS, ''
1441
1378
  else
1442
1379
  (callout_marks[lineno] ||= []) << m[3]
1443
1380
  last = lineno
1444
1381
  nil
1445
1382
  end
1446
1383
  }
1447
- } * EOL
1384
+ } * LF
1448
1385
  callout_on_last = (last == lineno)
1449
1386
  callout_marks = nil if callout_marks.empty?
1450
1387
  else
@@ -1456,7 +1393,7 @@ module Substitutors
1456
1393
 
1457
1394
  case highlighter
1458
1395
  when 'coderay'
1459
- if (linenums_mode = (attr? 'linenums') ? (@document.attributes['coderay-linenums-mode'] || :table).to_sym : nil)
1396
+ if (linenums_mode = (attr? 'linenums', nil, false) ? (@document.attributes['coderay-linenums-mode'] || :table).to_sym : nil)
1460
1397
  if attr? 'highlight', nil, false
1461
1398
  highlight_lines = resolve_highlight_lines(attr 'highlight', nil, false)
1462
1399
  end
@@ -1468,8 +1405,9 @@ module Substitutors
1468
1405
  :highlight_lines => highlight_lines,
1469
1406
  :bold_every => false}].highlight source
1470
1407
  when 'pygments'
1471
- lexer = ::Pygments::Lexer[attr('language', nil, false)] || ::Pygments::Lexer['text']
1472
- opts = { :cssclass => 'pyhl', :classprefix => 'tok-', :nobackground => true }
1408
+ lexer = ::Pygments::Lexer.find_by_alias(attr 'language', 'text', false) || ::Pygments::Lexer.find_by_mimetype('text/plain')
1409
+ opts = { :cssclass => 'pyhl', :classprefix => 'tok-', :nobackground => true, :stripnl => false }
1410
+ opts[:startinline] = !(option? 'mixed') if lexer.name == 'PHP'
1473
1411
  unless (@document.attributes['pygments-css'] || 'class') == 'class'
1474
1412
  opts[:noclasses] = true
1475
1413
  opts[:style] = (@document.attributes['pygments-style'] || Stylesheets::DEFAULT_PYGMENTS_STYLE)
@@ -1479,56 +1417,42 @@ module Substitutors
1479
1417
  opts[:hl_lines] = highlight_lines * ' '
1480
1418
  end
1481
1419
  end
1482
- if attr? 'linenums'
1483
- # TODO we could add the line numbers in ourselves instead of having to strip out the junk
1484
- # FIXME move these regular expressions into constants
1485
- if (opts[:linenos] = @document.attributes['pygments-linenums-mode'] || 'table') == 'table'
1486
- linenums_mode = :table
1487
- # NOTE these subs clean out HTML that messes up our styles
1488
- result = lexer.highlight(source, :options => opts).
1489
- sub(/<div class="pyhl">(.*)<\/div>/m, '\1').
1490
- gsub(/<pre[^>]*>(.*?)<\/pre>\s*/m, '\1')
1491
- else
1492
- result = lexer.highlight(source, :options => opts).
1493
- sub(/<div class="pyhl"><pre[^>]*>(.*?)<\/pre><\/div>/m, '\1')
1494
- end
1420
+ # TODO we could add the line numbers in ourselves instead of having to strip out the junk
1421
+ if (attr? 'linenums', nil, false) && (opts[:linenos] = @document.attributes['pygments-linenums-mode'] || 'table') == 'table'
1422
+ linenums_mode = :table
1423
+ result = lexer.highlight(source, :options => opts).sub(PygmentsWrapperDivRx, '\1').gsub(PygmentsWrapperPreRx, '\1')
1495
1424
  else
1496
- # nowrap gives us just the highlighted source; won't work when we need linenums though
1497
- opts[:nowrap] = true
1498
- result = lexer.highlight(source, :options => opts)
1425
+ if PygmentsWrapperPreRx =~ (result = lexer.highlight(source, :options => opts))
1426
+ result = $1
1427
+ end
1499
1428
  end
1500
1429
  end
1501
1430
 
1502
1431
  # fix passthrough placeholders that got caught up in syntax highlighting
1503
1432
  unless @passthroughs.empty?
1504
- result = result.gsub PASS_MATCH_HI, %(#{PASS_START}\\1#{PASS_END})
1433
+ result = result.gsub HighlightedPassSlotRx, %(#{PASS_START}\\1#{PASS_END})
1505
1434
  end
1506
1435
 
1507
1436
  if process_callouts && callout_marks
1508
1437
  lineno = 0
1509
1438
  reached_code = linenums_mode != :table
1510
- result.split(EOL).map {|line|
1439
+ result.split(LF, -1).map {|line|
1511
1440
  unless reached_code
1512
- unless line.include?('<td class="code">')
1513
- next line
1514
- end
1441
+ next line unless line.include?('<td class="code">')
1515
1442
  reached_code = true
1516
1443
  end
1517
- lineno = lineno + 1
1444
+ lineno += 1
1518
1445
  if (conums = callout_marks.delete(lineno))
1519
1446
  tail = nil
1520
- if callout_on_last && callout_marks.empty?
1521
- # QUESTION when does this happen?
1522
- if (pos = line.index '</pre>')
1523
- tail = line[pos..-1]
1524
- line = %(#{line[0...pos].chomp ' '} )
1525
- else
1526
- # Give conum on final line breathing room if trailing space in source is dropped
1527
- line = %(#{line.chomp ' '} )
1447
+ if callout_on_last && callout_marks.empty? && linenums_mode == :table
1448
+ if highlighter == 'coderay' && (pos = line.index '</pre>')
1449
+ line, tail = (line.slice 0, pos), (line.slice pos, line.length)
1450
+ elsif highlighter == 'pygments' && (pos = line.start_with? '</td>')
1451
+ line, tail = '', line
1528
1452
  end
1529
1453
  end
1530
1454
  if conums.size == 1
1531
- %(#{line}#{Inline.new(self, :callout, conums[0], :id => @document.callouts.read_next_id).convert }#{tail})
1455
+ %(#{line}#{Inline.new(self, :callout, conums[0], :id => @document.callouts.read_next_id).convert}#{tail})
1532
1456
  else
1533
1457
  conums_markup = conums.map {|conum| Inline.new(self, :callout, conum, :id => @document.callouts.read_next_id).convert } * ' '
1534
1458
  %(#{line}#{conums_markup}#{tail})
@@ -1536,7 +1460,7 @@ module Substitutors
1536
1460
  else
1537
1461
  line
1538
1462
  end
1539
- } * EOL
1463
+ } * LF
1540
1464
  else
1541
1465
  result
1542
1466
  end
@@ -1545,7 +1469,7 @@ module Substitutors
1545
1469
  # e.g., highlight="1-5, !2, 10" or highlight=1-5;!2,10
1546
1470
  def resolve_highlight_lines spec
1547
1471
  lines = []
1548
- spec.delete(' ').split(DataDelimiterRx).map do |entry|
1472
+ ((spec.include? ' ') ? (spec.delete ' ') : spec).split(DataDelimiterRx).map do |entry|
1549
1473
  negate = false
1550
1474
  if entry.start_with? '!'
1551
1475
  entry = entry[1..-1]
@@ -1577,54 +1501,48 @@ module Substitutors
1577
1501
  #
1578
1502
  # returns the substituted source
1579
1503
  def sub_source source, process_callouts
1580
- return process_callouts ? sub_callouts(sub_specialchars(source)) : sub_specialchars(source)
1504
+ process_callouts ? sub_callouts(sub_specialchars source) : (sub_specialchars source)
1581
1505
  end
1582
1506
 
1583
1507
  # Internal: Lock-in the substitutions for this block
1584
1508
  #
1585
- # Looks for an attribute named "subs". If present, resolves the
1586
- # substitutions and assigns it to the subs property on this block.
1587
- # Otherwise, assigns a set of default substitutions based on the
1588
- # content model of the block.
1509
+ # Looks for an attribute named "subs". If present, resolves substitutions
1510
+ # from the value of that attribute and assigns them to the subs property on
1511
+ # this block. Otherwise, uses the substitutions assigned to the default_subs
1512
+ # property, if specified, or selects a default set of substitutions based on
1513
+ # the content model of the block.
1589
1514
  #
1590
- # Returns nothing
1515
+ # Returns The Array of resolved substitutions now assigned to this block
1591
1516
  def lock_in_subs
1592
- if @default_subs
1593
- default_subs = @default_subs
1594
- else
1517
+ unless (default_subs = @default_subs)
1595
1518
  case @content_model
1596
1519
  when :simple
1597
- default_subs = SUBS[:normal]
1520
+ default_subs = NORMAL_SUBS
1598
1521
  when :verbatim
1599
1522
  if @context == :listing || (@context == :literal && !(option? 'listparagraph'))
1600
- default_subs = SUBS[:verbatim]
1523
+ default_subs = VERBATIM_SUBS
1601
1524
  elsif @context == :verse
1602
- default_subs = SUBS[:normal]
1525
+ default_subs = NORMAL_SUBS
1603
1526
  else
1604
- default_subs = SUBS[:basic]
1527
+ default_subs = BASIC_SUBS
1605
1528
  end
1606
1529
  when :raw
1607
- if @context == :stem
1608
- default_subs = SUBS[:basic]
1609
- else
1610
- default_subs = SUBS[:pass]
1611
- end
1530
+ # TODO make pass subs a compliance setting; AsciiDoc Python performs :attributes and :macros on a pass block
1531
+ default_subs = @context == :stem ? BASIC_SUBS : NONE_SUBS
1612
1532
  else
1613
- return
1533
+ return @subs
1614
1534
  end
1615
1535
  end
1616
1536
 
1617
- if (custom_subs = @attributes['subs'])
1618
- @subs = resolve_block_subs custom_subs, default_subs, @context
1619
- else
1620
- @subs = default_subs.dup
1621
- end
1537
+ @subs = (custom_subs = @attributes['subs']) ? (resolve_block_subs custom_subs, default_subs, @context) : default_subs.dup
1622
1538
 
1623
1539
  # QUESION delegate this logic to a method?
1624
- if @context == :listing && @style == 'source' && @attributes['language'] &&
1625
- @document.basebackend?('html') && SUB_HIGHLIGHT.include?(@document.attributes['source-highlighter'])
1626
- @subs = @subs.map {|sub| sub == :specialcharacters ? :highlight : sub }
1540
+ if @context == :listing && @style == 'source' && (@attributes.key? 'language') && (@document.basebackend? 'html') &&
1541
+ (SUB_HIGHLIGHT.include? @document.attributes['source-highlighter']) && (idx = @subs.index :specialcharacters)
1542
+ @subs[idx] = :highlight
1627
1543
  end
1544
+
1545
+ @subs
1628
1546
  end
1629
1547
  end
1630
1548
  end