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
@@ -1,259 +0,0 @@
1
- module Asciidoctor
2
- # Public: Methods for rendering Asciidoc Documents, Sections, and Blocks
3
- # using eRuby templates.
4
- class Renderer
5
- RE_ASCIIDOCTOR_NAMESPACE = /^Asciidoctor::/
6
- RE_TEMPLATE_CLASS_SUFFIX = /Template$/
7
- RE_CAMELCASE_BOUNDARY_1 = /([[:upper:]]+)([[:upper:]][[:alpha:]])/
8
- RE_CAMELCASE_BOUNDARY_2 = /([[:lower:]])([[:upper:]])/
9
-
10
- attr_reader :compact
11
- attr_reader :cache
12
-
13
- @@global_cache = nil
14
-
15
- # Public: Initialize an Asciidoctor::Renderer object.
16
- #
17
- def initialize(options={})
18
- @debug = !!options[:debug]
19
-
20
- @views = {}
21
- @compact = options[:compact]
22
- @cache = nil
23
-
24
- backend = options[:backend]
25
- case backend
26
- when 'html5', 'docbook45', 'docbook5'
27
- eruby = load_eruby options[:eruby]
28
- #Helpers.require_library 'asciidoctor/backends/' + backend
29
- require 'asciidoctor/backends/' + backend
30
- # Load up all the template classes that we know how to render for this backend
31
- BaseTemplate.template_classes.each do |tc|
32
- if tc.to_s.downcase.include?('::' + backend + '::') # optimization
33
- view_name, view_backend = self.class.extract_view_mapping(tc)
34
- if view_backend == backend
35
- @views[view_name] = tc.new(view_name, backend, eruby)
36
- end
37
- end
38
- end
39
- else
40
- Debug.debug { "No built-in templates for backend: #{backend}" }
41
- end
42
-
43
- # If user passed in a template dir, let them override our base templates
44
- if (template_dirs = options.delete(:template_dirs))
45
- Helpers.require_library 'tilt', true
46
-
47
- if (template_cache = options[:template_cache]) === true
48
- # FIXME probably want to use our own cache object for more control
49
- @cache = (@@global_cache ||= TemplateCache.new)
50
- elsif template_cache
51
- @cache = template_cache
52
- end
53
-
54
- view_opts = {
55
- :erb => { :trim => '<>' },
56
- :haml => { :format => :xhtml, :attr_wrapper => '"', :ugly => true, :escape_attrs => false },
57
- :slim => { :disable_escape => true, :sort_attrs => false, :pretty => false }
58
- }
59
-
60
- # workaround until we have a proper way to configure
61
- if {'html5' => true, 'dzslides' => true, 'deckjs' => true, 'revealjs' => true}.has_key? backend
62
- view_opts[:haml][:format] = view_opts[:slim][:format] = :html5
63
- end
64
-
65
- slim_loaded = false
66
- path_resolver = PathResolver.new
67
- engine = options[:template_engine]
68
-
69
- template_dirs.each do |template_dir|
70
- # TODO need to think about safe mode restrictions here
71
- template_dir = path_resolver.system_path template_dir, nil
72
- template_glob = '*'
73
- if engine
74
- template_glob = "*.#{engine}"
75
- # example: templates/haml
76
- if File.directory? File.join(template_dir, engine)
77
- template_dir = File.join template_dir, engine
78
- end
79
- end
80
-
81
- # example: templates/html5 or templates/haml/html5
82
- if File.directory? File.join(template_dir, backend)
83
- template_dir = File.join template_dir, backend
84
- end
85
-
86
- # skip scanning folder if we've already done it for same backend/engine
87
- if @cache && @cache.cached?(:scan, template_dir, template_glob)
88
- @views.update(@cache.fetch :scan, template_dir, template_glob)
89
- next
90
- end
91
-
92
- helpers = nil
93
- scan_result = {}
94
- # Grab the files in the top level of the directory (we're not traversing)
95
- Dir.glob(File.join(template_dir, template_glob)).
96
- select{|f| File.file? f }.each do |template|
97
- basename = File.basename(template)
98
- if basename == 'helpers.rb'
99
- helpers = template
100
- next
101
- end
102
- name_parts = basename.split('.')
103
- next if name_parts.size < 2
104
- view_name = name_parts.first
105
- ext_name = name_parts.last
106
- if ext_name == 'slim' && !slim_loaded
107
- # slim doesn't get loaded by Tilt
108
- Helpers.require_library 'slim', true
109
- end
110
- next unless Tilt.registered? ext_name
111
- opts = view_opts[ext_name.to_sym]
112
- if @cache
113
- @views[view_name] = scan_result[view_name] = @cache.fetch(:view, template) {
114
- Tilt.new(template, nil, opts)
115
- }
116
- else
117
- @views[view_name] = Tilt.new template, nil, opts
118
- end
119
- end
120
-
121
- require helpers unless helpers.nil?
122
- @cache.store(scan_result, :scan, template_dir, template_glob) if @cache
123
- end
124
- end
125
- end
126
-
127
- # Public: Render an Asciidoc object with a specified view template.
128
- #
129
- # view - the String view template name.
130
- # object - the Object to be used as an evaluation scope.
131
- # locals - the optional Hash of locals to be passed to Tilt (default {}) (also ignored, really)
132
- def render(view, object, locals = {})
133
- if !@views.has_key? view
134
- raise "Couldn't find a view in @views for #{view}"
135
- end
136
-
137
- @views[view].render(object, locals)
138
- end
139
-
140
- def views
141
- readonly_views = @views.dup
142
- readonly_views.freeze
143
- readonly_views
144
- end
145
-
146
- def register_view(view_name, tilt_template)
147
- # TODO need to figure out how to cache this
148
- @views[view_name] = tilt_template
149
- end
150
-
151
- # Internal: Load the eRuby implementation
152
- #
153
- # name - the String name of the eRuby implementation (default: 'erb')
154
- #
155
- # returns the eRuby implementation class
156
- def load_eruby(name)
157
- if name.nil? || !['erb', 'erubis'].include?(name)
158
- name = 'erb'
159
- end
160
-
161
- if name == 'erb'
162
- Helpers.require_library 'erb'
163
- ::ERB
164
- elsif name == 'erubis'
165
- Helpers.require_library 'erubis', true
166
- ::Erubis::FastEruby
167
- end
168
- end
169
-
170
- # TODO better name for this method (and/or field)
171
- def self.global_cache
172
- @@global_cache
173
- end
174
-
175
- # TODO better name for this method (and/or field)
176
- def self.reset_global_cache
177
- @@global_cache.clear if @@global_cache
178
- end
179
-
180
- # Internal: Extracts the view name and backend from a qualified Ruby class
181
- #
182
- # The purpose of this method is to determine the view name and backend to
183
- # which a built-in template class maps. We can make certain assumption since
184
- # we have control over these class names. The Asciidoctor:: prefix and
185
- # Template suffix are stripped as the first step in the conversion.
186
- #
187
- # qualified_class - The Class or String qualified class name from which to extract the view name and backend
188
- #
189
- # Examples
190
- #
191
- # Renderer.extract_view_mapping(Asciidoctor::HTML5::DocumentTemplate)
192
- # # => ['document', 'html5']
193
- #
194
- # Renderer.extract_view_mapping(Asciidoctor::DocBook45::BlockSidebarTemplate)
195
- # # => ['block_sidebar', 'docbook45']
196
- #
197
- # Returns A two-element String Array mapped as [view_name, backend], where backend may be nil
198
- def self.extract_view_mapping(qualified_class)
199
- view_name, backend = qualified_class.to_s.
200
- sub(RE_ASCIIDOCTOR_NAMESPACE, '').
201
- sub(RE_TEMPLATE_CLASS_SUFFIX, '').
202
- split('::').reverse
203
- view_name = camelcase_to_underscore(view_name)
204
- backend = backend.downcase unless backend.nil?
205
- [view_name, backend]
206
- end
207
-
208
- # Internal: Convert a CamelCase word to an underscore-delimited word
209
- #
210
- # Examples
211
- #
212
- # Renderer.camelcase_to_underscore('BlockSidebar')
213
- # # => 'block_sidebar'
214
- #
215
- # Renderer.camelcase_to_underscore('BlockUlist')
216
- # # => 'block_ulist'
217
- #
218
- # Returns the String converted from CamelCase to underscore-delimited
219
- def self.camelcase_to_underscore(str)
220
- str.gsub(RE_CAMELCASE_BOUNDARY_1, '\1_\2').
221
- gsub(RE_CAMELCASE_BOUNDARY_2, '\1_\2').downcase
222
- end
223
-
224
- end
225
-
226
- class TemplateCache
227
- attr_reader :cache
228
-
229
- def initialize
230
- @cache = {}
231
- end
232
-
233
- # check if a key is available in the cache
234
- def cached? *key
235
- @cache.has_key? key
236
- end
237
-
238
- # retrieves an item from the cache stored in the cache key
239
- # if a block is given, the block is called and the return
240
- # value stored in the cache under the specified key
241
- def fetch(*key)
242
- if block_given?
243
- @cache[key] ||= yield
244
- else
245
- @cache[key]
246
- end
247
- end
248
-
249
- # stores an item in the cache under the specified key
250
- def store(value, *key)
251
- @cache[key] = value
252
- end
253
-
254
- # Clears the cache
255
- def clear
256
- @cache = {}
257
- end
258
- end
259
- end
@@ -1,1083 +0,0 @@
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 Substituters
6
-
7
- SUBS = {
8
- :basic => [:specialcharacters],
9
- :normal => [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements],
10
- :verbatim => [:specialcharacters, :callouts],
11
- :title => [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements],
12
- :header => [:specialcharacters, :attributes],
13
- :pass => [:attributes, :macros]
14
- }
15
-
16
- COMPOSITE_SUBS = {
17
- :none => [],
18
- :normal => SUBS[:normal],
19
- :verbatim => SUBS[:verbatim]
20
- }
21
-
22
- SUB_OPTIONS = {
23
- :block => COMPOSITE_SUBS.keys + SUBS[:normal] + [:callouts],
24
- :inline => COMPOSITE_SUBS.keys + SUBS[:normal]
25
- }
26
-
27
- # Internal: A String Array of passthough (unprocessed) text captured from this block
28
- attr_reader :passthroughs
29
-
30
- # Public: Apply the specified substitutions to the lines of text
31
- #
32
- # source - The String or String Array of text to process
33
- # subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default: :normal)
34
- # expand - A Boolean to control whether sub aliases are expanded (default: true)
35
- #
36
- # returns Either a String or String Array, whichever matches the type of the first argument
37
- def apply_subs source, subs = :normal, expand = false
38
- if subs == :normal
39
- subs = SUBS[:normal]
40
- elsif subs.nil?
41
- return source
42
- elsif expand
43
- if subs.is_a? Symbol
44
- subs = COMPOSITE_SUBS[subs] || [subs]
45
- else
46
- effective_subs = []
47
- subs.each do |key|
48
- if COMPOSITE_SUBS.has_key? key
49
- effective_subs.push(*COMPOSITE_SUBS[key])
50
- else
51
- effective_subs << key
52
- end
53
- end
54
-
55
- subs = effective_subs
56
- end
57
- end
58
-
59
- return source if subs.empty?
60
-
61
- multiline = source.is_a?(Array)
62
- text = multiline ? source.join : source
63
-
64
- if (has_passthroughs = subs.include?(:macros))
65
- text = extract_passthroughs(text)
66
- end
67
-
68
- subs.each do |type|
69
- case type
70
- when :specialcharacters
71
- text = sub_specialcharacters(text)
72
- when :quotes
73
- text = sub_quotes(text)
74
- when :attributes
75
- text = sub_attributes(text.lines.entries).join
76
- when :replacements
77
- text = sub_replacements(text)
78
- when :macros
79
- text = sub_macros(text)
80
- when :highlight
81
- text = highlight_source(text, subs.include?(:callouts))
82
- when :callouts
83
- text = sub_callouts(text) unless subs.include?(:highlight)
84
- when :post_replacements
85
- text = sub_post_replacements(text)
86
- else
87
- warn "asciidoctor: WARNING: unknown substitution type #{type}"
88
- end
89
- end
90
- text = restore_passthroughs(text) if has_passthroughs
91
-
92
- multiline ? text.lines.entries : text
93
- end
94
-
95
- # Public: Apply normal substitutions.
96
- #
97
- # lines - The lines of text to process. Can be a String or a String Array
98
- #
99
- # returns - A String with normal substitutions performed
100
- def apply_normal_subs(lines)
101
- apply_subs lines.is_a?(Array) ? lines.join : lines
102
- end
103
-
104
- # Public: Apply substitutions for titles.
105
- #
106
- # title - The String title to process
107
- #
108
- # returns - A String with title substitutions performed
109
- def apply_title_subs(title)
110
- apply_subs title, SUBS[:title]
111
- end
112
-
113
- # Public: Apply substitutions for header metadata and attribute assignments
114
- #
115
- # text - String containing the text process
116
- #
117
- # returns - A String with header substitutions performed
118
- def apply_header_subs(text)
119
- apply_subs text, SUBS[:header]
120
- end
121
-
122
- =begin
123
- # Public: Apply explicit substitutions, if specified, otherwise normal substitutions.
124
- #
125
- # lines - The lines of text to process. Can be a String or a String Array
126
- #
127
- # returns - A String with substitutions applied
128
- def apply_para_subs(lines)
129
- if (subs = attr('subs', nil, false))
130
- apply_subs lines.join, resolve_subs(subs)
131
- else
132
- apply_subs lines.join
133
- end
134
- end
135
-
136
- # Public: Apply substitutions for titles
137
- #
138
- # lines - A String Array containing the lines of text process
139
- #
140
- # returns - A String with literal (verbatim) substitutions performed
141
- def apply_literal_subs(lines)
142
- if (subs = attr('subs', nil, false))
143
- apply_subs lines.join, resolve_subs(subs)
144
- elsif @style == 'source' && @document.attributes['basebackend'] == 'html' &&
145
- ((highlighter = @document.attributes['source-highlighter']) == 'coderay' ||
146
- highlighter == 'pygments') && attr?('language')
147
- highlight_source lines.join, highlighter, callouts
148
- else
149
- apply_subs lines.join, SUBS[:verbatim]
150
- end
151
- end
152
-
153
- # Public: Apply substitutions for passthrough text
154
- #
155
- # lines - A String Array containing the lines of text process
156
- #
157
- # returns - A String with passthrough substitutions performed
158
- def apply_passthrough_subs(lines)
159
- if (subs = attr('subs', nil, false))
160
- subs = resolve_subs(subs)
161
- else
162
- subs = SUBS[:pass]
163
- end
164
- apply_subs lines.join, subs
165
- end
166
- =end
167
-
168
- # Internal: Extract the passthrough text from the document for reinsertion after processing.
169
- #
170
- # text - The String from which to extract passthrough fragements
171
- #
172
- # returns - The text with the passthrough region substituted with placeholders
173
- def extract_passthroughs(text)
174
- result = text.dup
175
-
176
- result.gsub!(REGEXP[:pass_macro]) {
177
- # alias match for Ruby 1.8.7 compat
178
- m = $~
179
- # honor the escape
180
- if m[0].start_with? '\\'
181
- next m[0][1..-1]
182
- end
183
-
184
- if m[1] == '$$'
185
- subs = [:specialcharacters]
186
- elsif m[3].nil? || m[3].empty?
187
- subs = []
188
- else
189
- subs = resolve_pass_subs m[3]
190
- end
191
-
192
- # TODO move unescaping closing square bracket to an operation
193
- @passthroughs << {:text => m[2] || m[4].gsub('\]', ']'), :subs => subs}
194
- index = @passthroughs.size - 1
195
- "\e#{index}\e"
196
- } unless !(result.include?('+++') || result.include?('$$') || result.include?('pass:'))
197
-
198
- result.gsub!(REGEXP[:pass_lit]) {
199
- # alias match for Ruby 1.8.7 compat
200
- m = $~
201
-
202
- unescaped_attrs = nil
203
- # honor the escape
204
- if m[3].start_with? '\\'
205
- next m[2].nil? ? "#{m[1]}#{m[3][1..-1]}" : "#{m[1]}[#{m[2]}]#{m[3][1..-1]}"
206
- elsif m[1] == '\\' && !m[2].nil?
207
- unescaped_attrs = "[#{m[2]}]"
208
- end
209
-
210
- if unescaped_attrs.nil? && !m[2].nil?
211
- attributes = parse_attributes(m[2])
212
- else
213
- attributes = {}
214
- end
215
-
216
- @passthroughs << {:text => m[4], :subs => [:specialcharacters], :attributes => attributes, :literal => true}
217
- index = @passthroughs.size - 1
218
- "#{unescaped_attrs || m[1]}\e#{index}\e"
219
- } unless !result.include?('`')
220
-
221
- result
222
- end
223
-
224
- # Internal: Restore the passthrough text by reinserting into the placeholder positions
225
- #
226
- # text - The String text into which to restore the passthrough text
227
- #
228
- # returns The String text with the passthrough text restored
229
- def restore_passthroughs(text)
230
- return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\e")
231
-
232
- text.gsub(REGEXP[:pass_placeholder]) {
233
- pass = @passthroughs[$1.to_i];
234
- text = apply_subs(pass[:text], pass.fetch(:subs, []))
235
- pass[:literal] ? Inline.new(self, :quoted, text, :type => :monospaced, :attributes => pass.fetch(:attributes, {})).render : text
236
- }
237
- end
238
-
239
- # Public: Substitute special characters (i.e., encode XML)
240
- #
241
- # Special characters are defined in the Asciidoctor::SPECIAL_CHARS Array constant
242
- #
243
- # text - The String text to process
244
- #
245
- # returns The String text with special characters replaced
246
- def sub_specialcharacters(text)
247
- # this syntax only available in Ruby 1.9
248
- #text.gsub(SPECIAL_CHARS_PATTERN, SPECIAL_CHARS)
249
-
250
- text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] }
251
- end
252
- alias :sub_specialchars :sub_specialcharacters
253
-
254
- # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc)
255
- #
256
- # text - The String text to process
257
- #
258
- # returns The String text with quoted text rendered using the backend templates
259
- def sub_quotes(text)
260
- result = text.dup
261
-
262
- QUOTE_SUBS.each {|type, scope, pattern|
263
- result.gsub!(pattern) { transform_quoted_text($~, type, scope) }
264
- }
265
-
266
- result
267
- end
268
-
269
- # Public: Substitute replacement characters (e.g., copyright, trademark, etc)
270
- #
271
- # text - The String text to process
272
- #
273
- # returns The String text with the replacement characters substituted
274
- def sub_replacements(text)
275
- result = text.dup
276
-
277
- REPLACEMENTS.each {|pattern, replacement, restore|
278
- result.gsub!(pattern) {
279
- matched = $&
280
- head = $1
281
- tail = $2
282
- if matched.include?('\\')
283
- matched.tr('\\', '')
284
- else
285
- case restore
286
- when :none
287
- replacement
288
- when :leading
289
- "#{head}#{replacement}"
290
- when :bounding
291
- "#{head}#{replacement}#{tail}"
292
- end
293
- end
294
- }
295
- }
296
-
297
- result
298
- end
299
-
300
- # Public: Substitute attribute references
301
- #
302
- # Attribute references are in the format {name}.
303
- #
304
- # If an attribute referenced in the line is missing, the line is dropped.
305
- #
306
- # text - The String text to process
307
- #
308
- # returns The String text with the attribute references replaced with attribute values
309
- #--
310
- # NOTE it's necessary to perform this substitution line-by-line
311
- # so that a missing key doesn't wipe out the whole block of data
312
- def sub_attributes(data, opts = {})
313
- return data if data.nil? || data.empty?
314
-
315
- string_data = data.is_a? String
316
- # normalizes data type to an array (string becomes single-element array)
317
- lines = string_data ? [data] : data
318
-
319
- result = []
320
- lines.each {|line|
321
- reject = false
322
- line = line.gsub(REGEXP[:attr_ref]) {
323
- # alias match for Ruby 1.8.7 compat
324
- m = $~
325
- # escaped attribute, return unescaped
326
- if !m[1].nil? || !m[4].nil?
327
- "{#{m[2]}}"
328
- elsif (directive = m[3])
329
- offset = directive.length + 1
330
- expr = m[2][offset..-1]
331
- case directive
332
- when 'set'
333
- args = expr.split(':')
334
- _, value = Lexer::store_attribute(args[0], args[1] || '', @document)
335
- if value.nil?
336
- # since this is an assignment, only drop-line applies here (skip and drop imply the same result)
337
- if @document.attributes.fetch('attribute-undefined', COMPLIANCE[:attribute_undefined]) == 'drop-line'
338
- Debug.debug { "Undefining attribute: #{key}, line marked for removal" }
339
- break ''
340
- end
341
- end
342
- ''
343
- when 'counter', 'counter2'
344
- args = expr.split(':')
345
- val = @document.counter(args[0], args[1])
346
- directive == 'counter2' ? '' : val
347
- else
348
- # if we get here, our attr_ref regex is too loose
349
- warn "asciidoctor: WARNING: illegal attribute directive: #{m[2]}"
350
- ''
351
- end
352
- elsif (key = m[2].downcase) && @document.attributes.has_key?(key)
353
- @document.attributes[key]
354
- elsif INTRINSICS.has_key? key
355
- INTRINSICS[key]
356
- else
357
- case (opts[:attribute_missing] || @document.attributes.fetch('attribute-missing', COMPLIANCE[:attribute_missing]))
358
- when 'skip'
359
- "{#{key}}"
360
- when 'drop-line'
361
- Debug.debug { "Missing attribute: #{key}, line marked for removal" }
362
- break ''
363
- else # 'drop'
364
- ''
365
- end
366
- end
367
- } if line.include? '{'
368
-
369
- result << line unless reject
370
- }
371
-
372
- string_data ? result.join : result
373
- end
374
-
375
- # Public: Substitute inline macros (e.g., links, images, etc)
376
- #
377
- # Replace inline macros, which may span multiple lines, in the provided text
378
- #
379
- # text - The String text to process
380
- #
381
- # returns The String with the inline macros rendered using the backend templates
382
- def sub_macros(text)
383
- return text if text.nil? || text.empty?
384
-
385
- result = text.dup
386
-
387
- # some look ahead assertions to cut unnecessary regex calls
388
- found = {}
389
- found[:square_bracket] = result.include?('[')
390
- found[:round_bracket] = result.include?('(')
391
- found[:colon] = result.include?(':')
392
- found[:at] = result.include?('@')
393
- found[:macroish] = (found[:square_bracket] && found[:colon])
394
- found[:macroish_short_form] = (found[:square_bracket] && found[:colon] && result.include?(':['))
395
- found[:uri] = (found[:colon] && result.include?('://'))
396
- use_link_attrs = @document.attributes.has_key?('linkattrs')
397
- experimental = @document.attributes.has_key?('experimental')
398
-
399
- if experimental
400
- if found[:macroish_short_form] && (result.include?('kbd:') || result.include?('btn:'))
401
- result.gsub!(REGEXP[:kbd_btn_macro]) {
402
- # alias match for Ruby 1.8.7 compat
403
- m = $~
404
- # honor the escape
405
- if (captured = m[0]).start_with? '\\'
406
- next captured[1..-1]
407
- end
408
-
409
- if captured.start_with?('kbd')
410
- keys = unescape_bracketed_text m[1]
411
-
412
- if keys == '+'
413
- keys = ['+']
414
- else
415
- # need to use closure to work around lack of negative lookbehind
416
- keys = keys.split(REGEXP[:kbd_delim]).inject([]) {|c, key|
417
- if key.end_with?('++')
418
- c << key[0..-3].strip
419
- c << '+'
420
- else
421
- c << key.strip
422
- end
423
- c
424
- }
425
- end
426
- Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).render
427
- elsif captured.start_with?('btn')
428
- label = unescape_bracketed_text m[1]
429
- Inline.new(self, :button, label).render
430
- end
431
- }
432
- end
433
-
434
- if found[:macroish] && result.include?('menu:')
435
- result.gsub!(REGEXP[:menu_macro]) {
436
- # alias match for Ruby 1.8.7 compat
437
- m = $~
438
- # honor the escape
439
- if (captured = m[0]).start_with? '\\'
440
- next captured[1..-1]
441
- end
442
-
443
- menu = m[1]
444
- items = m[2]
445
-
446
- if items.nil?
447
- submenus = []
448
- menuitem = nil
449
- else
450
- if (delim = items.include?('&gt;') ? '&gt;' : (items.include?(',') ? ',' : nil))
451
- submenus = items.split(delim).map(&:strip)
452
- menuitem = submenus.pop
453
- else
454
- submenus = []
455
- menuitem = items.rstrip
456
- end
457
- end
458
-
459
- Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render
460
- }
461
- end
462
-
463
- if result.include?('"') && result.include?('&gt;')
464
- result.gsub!(REGEXP[:menu_inline_macro]) {
465
- # alias match for Ruby 1.8.7 compat
466
- m = $~
467
- # honor the escape
468
- if (captured = m[0]).start_with? '\\'
469
- next captured[1..-1]
470
- end
471
-
472
- input = m[1]
473
-
474
- menu, *submenus = input.split('&gt;').map(&:strip)
475
- menuitem = submenus.pop
476
- Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render
477
- }
478
- end
479
- end
480
-
481
- # FIXME this location is somewhat arbitrary, probably need to be able to control ordering
482
- # TODO this handling needs some cleanup
483
- if (extensions = @document.extensions) && extensions.inline_macros? && found[:macroish]
484
- extensions.load_inline_macro_processors(@document).each do |processor|
485
- result.gsub!(processor.regexp) {
486
- # alias match for Ruby 1.8.7 compat
487
- m = $~
488
- # honor the escape
489
- if m[0].start_with? '\\'
490
- next m[0][1..-1]
491
- end
492
-
493
- target = m[1]
494
- if processor.options[:short_form]
495
- attributes = {}
496
- else
497
- posattrs = processor.options.fetch(:pos_attrs, [])
498
- attributes = parse_attributes(m[2], posattrs, :sub_input => true, :unescape_input => true)
499
- end
500
- processor.process self, target, attributes
501
- }
502
- end
503
- end
504
-
505
- if found[:macroish] && (result.include?('image:') || result.include?('icon:'))
506
- # image:filename.png[Alt Text]
507
- result.gsub!(REGEXP[:image_macro]) {
508
- # alias match for Ruby 1.8.7 compat
509
- m = $~
510
- # honor the escape
511
- if m[0].start_with? '\\'
512
- next m[0][1..-1]
513
- end
514
-
515
- raw_attrs = unescape_bracketed_text m[2]
516
- if m[0].start_with? 'icon:'
517
- type = 'icon'
518
- posattrs = ['size']
519
- else
520
- type = 'image'
521
- posattrs = ['alt', 'width', 'height']
522
- end
523
- target = sub_attributes(m[1])
524
- unless type == 'icon'
525
- @document.register(:images, target)
526
- end
527
- attrs = parse_attributes(raw_attrs, posattrs)
528
- if !attrs['alt']
529
- attrs['alt'] = File.basename(target, File.extname(target))
530
- end
531
- Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).render
532
- }
533
- end
534
-
535
- if found[:macroish_short_form] || found[:round_bracket]
536
- # indexterm:[Tigers,Big cats]
537
- # (((Tigers,Big cats)))
538
- result.gsub!(REGEXP[:indexterm_macro]) {
539
- # alias match for Ruby 1.8.7 compat
540
- m = $~
541
- # honor the escape
542
- if m[0].start_with? '\\'
543
- next m[0][1..-1]
544
- end
545
-
546
- terms = unescape_bracketed_text(m[1] || m[2]).split(',').map(&:strip)
547
- @document.register(:indexterms, [*terms])
548
- Inline.new(self, :indexterm, text, :attributes => {'terms' => terms}).render
549
- }
550
-
551
- # indexterm2:[Tigers]
552
- # ((Tigers))
553
- result.gsub!(REGEXP[:indexterm2_macro]) {
554
- # alias match for Ruby 1.8.7 compat
555
- m = $~
556
- # honor the escape
557
- if m[0].start_with? '\\'
558
- next m[0][1..-1]
559
- end
560
-
561
- text = unescape_bracketed_text(m[1] || m[2])
562
- @document.register(:indexterms, [text])
563
- Inline.new(self, :indexterm, text, :type => :visible).render
564
- }
565
- end
566
-
567
- if found[:uri]
568
- # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>)
569
- result.gsub!(REGEXP[:link_inline]) {
570
- # alias match for Ruby 1.8.7 compat
571
- m = $~
572
- # honor the escape
573
- if m[2].start_with? '\\'
574
- next "#{m[1]}#{m[2][1..-1]}#{m[3]}"
575
- # not a valid macro syntax w/o trailing square brackets
576
- # we probably shouldn't even get here...our regex is doing too much
577
- elsif m[1] == 'link:' && m[3].nil?
578
- next m[0]
579
- end
580
- prefix = (m[1] != 'link:' ? m[1] : '')
581
- target = m[2]
582
- suffix = ''
583
- # strip the <> around the link
584
- if prefix.start_with?('&lt;') && target.end_with?('&gt;')
585
- prefix = prefix[4..-1]
586
- target = target[0..-5]
587
- elsif prefix.start_with?('(') && target.end_with?(')')
588
- target = target[0..-2]
589
- suffix = ')'
590
- elsif target.end_with?('):')
591
- target = target[0..-3]
592
- suffix = '):'
593
- end
594
- @document.register(:links, target)
595
-
596
- attrs = nil
597
- #text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : ''
598
- if !m[3].to_s.empty?
599
- if use_link_attrs && (m[3].start_with?('"') || m[3].include?(','))
600
- attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']')), [])
601
- text = attrs[1]
602
- else
603
- text = sub_attributes(m[3].gsub('\]', ']'))
604
- end
605
-
606
- if text.end_with? '^'
607
- text = text.chop
608
- attrs ||= {}
609
- attrs['window'] = '_blank' unless attrs.has_key?('window')
610
- end
611
- else
612
- text = ''
613
- end
614
-
615
- "#{prefix}#{Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target, :attributes => attrs).render}#{suffix}"
616
- }
617
- end
618
-
619
- if found[:macroish] && (result.include?('link:') || result.include?('mailto:'))
620
- # inline link macros, link:target[text]
621
- result.gsub!(REGEXP[:link_macro]) {
622
- # alias match for Ruby 1.8.7 compat
623
- m = $~
624
- # honor the escape
625
- if m[0].start_with? '\\'
626
- next m[0][1..-1]
627
- end
628
- raw_target = m[1]
629
- mailto = m[0].start_with?('mailto:')
630
- target = mailto ? "mailto:#{raw_target}" : raw_target
631
-
632
- attrs = nil
633
- #text = sub_attributes(m[2].gsub('\]', ']'))
634
- if use_link_attrs && (m[2].start_with?('"') || m[2].include?(','))
635
- attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']')), [])
636
- text = attrs[1]
637
- if mailto
638
- if attrs.has_key? 2
639
- target = "#{target}?subject=#{Helpers.encode_uri(attrs[2])}"
640
-
641
- if attrs.has_key? 3
642
- target = "#{target}&amp;body=#{Helpers.encode_uri(attrs[3])}"
643
- end
644
- end
645
- end
646
- else
647
- text = sub_attributes(m[2].gsub('\]', ']'))
648
- end
649
-
650
- if text.end_with? '^'
651
- text = text.chop
652
- attrs ||= {}
653
- attrs['window'] = '_blank' unless attrs.has_key?('window')
654
- end
655
-
656
- # QUESTION should a mailto be registered as an e-mail address?
657
- @document.register(:links, target)
658
-
659
- Inline.new(self, :anchor, (!text.empty? ? text : raw_target), :type => :link, :target => target, :attributes => attrs).render
660
- }
661
- end
662
-
663
- if found[:at]
664
- result.gsub!(REGEXP[:email_inline]) {
665
- # alias match for Ruby 1.8.7 compat
666
- m = $~
667
- address = m[0]
668
- case address[0..0]
669
- when '\\'
670
- next address[1..-1]
671
- when '>', ':'
672
- next address
673
- end
674
-
675
- target = "mailto:#{address}"
676
- # QUESTION should this be registered as an e-mail address?
677
- @document.register(:links, target)
678
-
679
- Inline.new(self, :anchor, address, :type => :link, :target => target).render
680
- }
681
- end
682
-
683
- if found[:macroish_short_form] && result.include?('footnote')
684
- result.gsub!(REGEXP[:footnote_macro]) {
685
- # alias match for Ruby 1.8.7 compat
686
- m = $~
687
- # honor the escape
688
- if m[0].start_with? '\\'
689
- next m[0][1..-1]
690
- end
691
- if m[1] == 'footnote'
692
- # hmmmm
693
- text = restore_passthroughs(m[2])
694
- id = nil
695
- index = @document.counter('footnote-number')
696
- @document.register(:footnotes, Document::Footnote.new(index, id, text))
697
- type = nil
698
- target = nil
699
- else
700
- id, text = m[2].split(',', 2).map(&:strip)
701
- if !text.nil?
702
- # hmmmm
703
- text = restore_passthroughs(text)
704
- index = @document.counter('footnote-number')
705
- @document.register(:footnotes, Document::Footnote.new(index, id, text))
706
- type = :ref
707
- target = nil
708
- else
709
- footnote = @document.references[:footnotes].find {|fn| fn.id == id }
710
- target = id
711
- id = nil
712
- index = footnote.index
713
- text = footnote.text
714
- type = :xref
715
- end
716
- end
717
- Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).render
718
- }
719
- end
720
-
721
- if found[:macroish] || result.include?('&lt;&lt;')
722
- result.gsub!(REGEXP[:xref_macro]) {
723
- # alias match for Ruby 1.8.7 compat
724
- m = $~
725
- # honor the escape
726
- if m[0].start_with? '\\'
727
- next m[0][1..-1]
728
- end
729
- if !m[1].nil?
730
- id, reftext = m[1].split(',', 2).map(&:strip)
731
- id.sub!(REGEXP[:dbl_quoted], '\2')
732
- reftext.sub!(REGEXP[:m_dbl_quoted], '\2') unless reftext.nil?
733
- else
734
- id = m[2]
735
- reftext = !m[3].empty? ? m[3] : nil
736
- end
737
-
738
- if id.include? '#'
739
- path, fragment = id.split('#')
740
- else
741
- path = nil
742
- fragment = id
743
- end
744
-
745
- # handles form: id
746
- if path.nil?
747
- refid = fragment
748
- target = "##{fragment}"
749
- # handles forms: doc#, doc.adoc#, doc#id and doc.adoc#id
750
- else
751
- path = Helpers.rootname(path)
752
- # the referenced path is this document, or its contents has been included in this document
753
- if @document.attr?('docname', path) || @document.references[:includes].include?(path)
754
- refid = fragment
755
- path = nil
756
- target = "##{fragment}"
757
- else
758
- refid = fragment.nil? ? path : "#{path}##{fragment}"
759
- path = "#{path}#{@document.attr 'outfilesuffix', '.html'}"
760
- target = fragment.nil? ? path : "#{path}##{fragment}"
761
- end
762
- end
763
- Inline.new(self, :anchor, reftext, :type => :xref, :target => target, :attributes => {'path' => path, 'fragment' => fragment, 'refid' => refid}).render
764
- }
765
- end
766
-
767
- if found[:square_bracket] && result.include?('[[[')
768
- result.gsub!(REGEXP[:biblio_macro]) {
769
- # alias match for Ruby 1.8.7 compat
770
- m = $~
771
- # honor the escape
772
- if m[0].start_with? '\\'
773
- next m[0][1..-1]
774
- end
775
- id = reftext = m[1]
776
- Inline.new(self, :anchor, reftext, :type => :bibref, :target => id).render
777
- }
778
- end
779
-
780
- if found[:square_bracket] && result.include?('[[')
781
- result.gsub!(REGEXP[:anchor_macro]) {
782
- # alias match for Ruby 1.8.7 compat
783
- m = $~
784
- # honor the escape
785
- if m[0].start_with? '\\'
786
- next m[0][1..-1]
787
- end
788
- id, reftext = m[1].split(',').map(&:strip)
789
- id.sub!(REGEXP[:dbl_quoted], '\2')
790
- if reftext.nil?
791
- reftext = "[#{id}]"
792
- else
793
- reftext.sub!(REGEXP[:m_dbl_quoted], '\2')
794
- end
795
- # NOTE the reftext should also match what's in our references dic
796
- if !@document.references[:ids].has_key? id
797
- Debug.debug { "Missing reference for anchor #{id}" }
798
- end
799
- Inline.new(self, :anchor, reftext, :type => :ref, :target => id).render
800
- }
801
- end
802
-
803
- result
804
- end
805
-
806
- # Public: Substitute callout references
807
- #
808
- # text - The String text to process
809
- #
810
- # returns The String with the callout references rendered using the backend templates
811
- def sub_callouts(text)
812
- text.gsub(REGEXP[:callout_render]) {
813
- # alias match for Ruby 1.8.7 compat
814
- m = $~
815
- # honor the escape
816
- if m[1] == '\\'
817
- # we have to do a sub since we aren't sure it's the first char
818
- next m[0].sub('\\', '')
819
- end
820
- Inline.new(self, :callout, m[3], :id => @document.callouts.read_next_id).render
821
- }
822
- end
823
-
824
- # Public: Substitute post replacements
825
- #
826
- # text - The String text to process
827
- #
828
- # returns The String with the post replacements rendered using the backend templates
829
- def sub_post_replacements(text)
830
- if @document.attributes['hardbreaks']
831
- lines = text.lines.entries
832
- return text if lines.size == 1
833
- last = lines.pop
834
- lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).render }.push(last) * EOL
835
- else
836
- text.gsub(REGEXP[:line_break]) { Inline.new(self, :break, $1, :type => :line).render }
837
- end
838
- end
839
-
840
- # Internal: Transform (render) a quoted text region
841
- #
842
- # match - The MatchData for the quoted text region
843
- # type - The quoting type (single, double, strong, emphasis, monospaced, etc)
844
- # scope - The scope of the quoting (constrained or unconstrained)
845
- #
846
- # returns The rendered text for the quoted text region
847
- def transform_quoted_text(match, type, scope)
848
- unescaped_attrs = nil
849
- if match[0].start_with? '\\'
850
- if scope == :constrained && !match[2].nil?
851
- unescaped_attrs = "[#{match[2]}]"
852
- else
853
- return match[0][1..-1]
854
- end
855
- end
856
-
857
- if scope == :constrained
858
- if unescaped_attrs.nil?
859
- attributes = parse_quoted_text_attributes(match[2])
860
- id = attributes.nil? ? nil : attributes.delete('id')
861
- "#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :id => id, :attributes => attributes).render}"
862
- else
863
- "#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], :type => type, :attributes => {}).render}"
864
- end
865
- else
866
- attributes = parse_quoted_text_attributes(match[1])
867
- id = attributes.nil? ? nil : attributes.delete('id')
868
- Inline.new(self, :quoted, match[2], :type => type, :id => id, :attributes => attributes).render
869
- end
870
- end
871
-
872
- # Internal: Parse the attributes that are defined on quoted text
873
- #
874
- # str - A String of unprocessed attributes (space-separated roles or the id/role shorthand syntax)
875
- #
876
- # returns nil if str is nil, an empty Hash if str is empty, otherwise a Hash of attributes (role and id only)
877
- def parse_quoted_text_attributes(str)
878
- return nil if str.nil?
879
- return {} if str.empty?
880
- str = sub_attributes(str) if str.include?('{')
881
- str = str.strip
882
- # for compliance, only consider first positional attribute
883
- str, _ = str.split(',', 2) if str.include?(',')
884
-
885
- if str.empty?
886
- {}
887
- elsif str.start_with?('.') || str.start_with?('#')
888
- segments = str.split('#', 2)
889
-
890
- if segments.length > 1
891
- id, *more_roles = segments[1].split('.')
892
- else
893
- id = nil
894
- more_roles = []
895
- end
896
-
897
- roles = segments[0].empty? ? [] : segments[0].split('.')
898
- if roles.length > 1
899
- roles.shift
900
- end
901
-
902
- if more_roles.length > 0
903
- roles.concat more_roles
904
- end
905
-
906
- attrs = {}
907
- attrs['id'] = id unless id.nil?
908
- attrs['role'] = roles.empty? ? nil : (roles * ' ')
909
- attrs
910
- else
911
- {'role' => str}
912
- end
913
- end
914
-
915
- # Internal: Parse the attributes in the attribute line
916
- #
917
- # attrline - A String of unprocessed attributes (key/value pairs)
918
- # posattrs - The keys for positional attributes
919
- #
920
- # returns nil if attrline is nil, an empty Hash if attrline is empty, otherwise a Hash of parsed attributes
921
- def parse_attributes(attrline, posattrs = ['role'], opts = {})
922
- return nil if attrline.nil?
923
- return {} if attrline.empty?
924
- attrline = @document.sub_attributes(attrline) if opts[:sub_input]
925
- attrline = unescape_bracketed_text(attrline) if opts[:unescape_input]
926
- block = nil
927
- if opts.fetch(:sub_result, true)
928
- # substitutions are only performed on attribute values if block is not nil
929
- block = self
930
- end
931
-
932
- if opts.has_key?(:into)
933
- AttributeList.new(attrline, block).parse_into(opts[:into], posattrs)
934
- else
935
- AttributeList.new(attrline, block).parse(posattrs)
936
- end
937
- end
938
-
939
- # Internal: Strip bounding whitespace, fold endlines and unescaped closing
940
- # square brackets from text extracted from brackets
941
- def unescape_bracketed_text(text)
942
- return '' if text.empty?
943
- text.strip.tr(EOL, ' ').gsub('\]', ']')
944
- end
945
-
946
- # Internal: Resolve the list of comma-delimited subs against the possible options.
947
- #
948
- # subs - A comma-delimited String of substitution aliases
949
- #
950
- # returns An Array of Symbols representing the substitution operation
951
- def resolve_subs subs, type = :block, subject = nil
952
- return [] if subs.nil? || subs.empty?
953
- candidates = []
954
- subs.split(',').each do |val|
955
- key = val.strip.to_sym
956
- # special case to disable callouts for inline subs
957
- if key == :verbatim && type == :inline
958
- candidates << :specialcharacters
959
- elsif COMPOSITE_SUBS.has_key? key
960
- candidates.push(*COMPOSITE_SUBS[key])
961
- else
962
- candidates << key
963
- end
964
- end
965
- # weed out invalid options and remove duplicates (first wins)
966
- resolved = candidates & SUB_OPTIONS[type]
967
- if (invalid = candidates - resolved).size > 0
968
- warn "asciidoctor: WARNING: invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : nil}#{subject}: #{invalid * ', '}"
969
- end
970
- resolved
971
- end
972
-
973
- def resolve_block_subs subs, subject
974
- resolve_subs subs, :block, subject
975
- end
976
-
977
- def resolve_pass_subs subs
978
- resolve_subs subs, :inline, 'passthrough macro'
979
- end
980
-
981
- # Public: Highlight the source code if a source highlighter is defined
982
- # on the document, otherwise return the text unprocessed
983
- #
984
- # Callout marks are stripped from the source prior to passing it to the
985
- # highlighter, then later restored in rendered form, so they are not
986
- # incorrectly processed by the source highlighter.
987
- #
988
- # source - the source code String to highlight
989
- # sub_callouts - a Boolean flag indicating whether callout marks should be substituted
990
- #
991
- # returns the highlighted source code, if a source highlighter is defined
992
- # on the document, otherwise the unprocessed text
993
- def highlight_source(source, sub_callouts, highlighter = nil)
994
- highlighter ||= @document.attributes['source-highlighter']
995
- Helpers.require_library highlighter, (highlighter == 'pygments' ? 'pygments.rb' : highlighter)
996
- callout_marks = {}
997
- lineno = 0
998
- callout_on_last = false
999
- if sub_callouts
1000
- last = -1
1001
- # extract callout marks, indexed by line number
1002
- source = source.split(EOL).map {|line|
1003
- lineno = lineno + 1
1004
- line.gsub(REGEXP[:callout_scan]) {
1005
- # alias match for Ruby 1.8.7 compat
1006
- m = $~
1007
- # honor the escape
1008
- if m[1] == '\\'
1009
- m[0].sub('\\', '')
1010
- else
1011
- (callout_marks[lineno] ||= []) << m[3]
1012
- last = lineno
1013
- nil
1014
- end
1015
- }
1016
- } * EOL
1017
- callout_on_last = (last == lineno)
1018
- end
1019
-
1020
- linenums_mode = nil
1021
-
1022
- case highlighter
1023
- when 'coderay'
1024
- result = ::CodeRay::Duo[attr('language', 'text').to_sym, :html, {
1025
- :css => @document.attributes.fetch('coderay-css', 'class').to_sym,
1026
- :line_numbers => (linenums_mode = (attr?('linenums') ? @document.attributes.fetch('coderay-linenums-mode', 'table').to_sym : nil)),
1027
- :line_number_anchors => false}].highlight(source)
1028
- when 'pygments'
1029
- lexer = ::Pygments::Lexer[attr('language')]
1030
- if lexer
1031
- opts = { :cssclass => 'pyhl', :classprefix => 'tok-', :nobackground => true }
1032
- opts[:noclasses] = true unless @document.attributes.fetch('pygments-css', 'class') == 'class'
1033
- if attr? 'linenums'
1034
- opts[:linenos] = (linenums_mode = @document.attributes.fetch('pygments-linenums-mode', 'table').to_sym).to_s
1035
- end
1036
-
1037
- # FIXME stick these regexs into constants
1038
- if linenums_mode == :table
1039
- result = lexer.highlight(source, :options => opts).
1040
- sub(/<div class="pyhl">(.*)<\/div>/m, '\1').
1041
- gsub(/<pre[^>]*>(.*?)<\/pre>\s*/m, '\1')
1042
- else
1043
- result = lexer.highlight(source, :options => opts).
1044
- sub(/<div class="pyhl"><pre[^>]*>(.*?)<\/pre><\/div>/m, '\1')
1045
- end
1046
- else
1047
- result = source
1048
- end
1049
- end
1050
-
1051
- if !sub_callouts || callout_marks.empty?
1052
- result
1053
- else
1054
- lineno = 0
1055
- reached_code = linenums_mode != :table
1056
- result.split(EOL).map {|line|
1057
- unless reached_code
1058
- unless line.include?('<td class="code">')
1059
- next line
1060
- end
1061
- reached_code = true
1062
- end
1063
- lineno = lineno + 1
1064
- if (conums = callout_marks.delete(lineno))
1065
- tail = nil
1066
- if callout_on_last && callout_marks.empty? && (pos = line.index '</pre>')
1067
- tail = line[pos..-1]
1068
- line = line[0...pos]
1069
- end
1070
- if conums.size == 1
1071
- %(#{line}#{Inline.new(self, :callout, conums.first, :id => @document.callouts.read_next_id).render }#{tail})
1072
- else
1073
- conums_markup = conums.map {|conum| Inline.new(self, :callout, conum, :id => @document.callouts.read_next_id).render } * ' '
1074
- %(#{line}#{conums_markup}#{tail})
1075
- end
1076
- else
1077
- line
1078
- end
1079
- } * EOL
1080
- end
1081
- end
1082
- end
1083
- end