asciidoctor 0.1.4 → 1.5.0

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