asciidoctor 0.1.3 → 0.1.4

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +387 -0
  3. data/README.adoc +358 -348
  4. data/asciidoctor.gemspec +30 -9
  5. data/bin/asciidoctor +3 -0
  6. data/bin/asciidoctor-safe +3 -0
  7. data/compat/asciidoc.conf +76 -4
  8. data/lib/asciidoctor.rb +174 -79
  9. data/lib/asciidoctor/abstract_block.rb +131 -101
  10. data/lib/asciidoctor/abstract_node.rb +108 -26
  11. data/lib/asciidoctor/attribute_list.rb +1 -1
  12. data/lib/asciidoctor/backends/_stylesheets.rb +204 -62
  13. data/lib/asciidoctor/backends/base_template.rb +11 -22
  14. data/lib/asciidoctor/backends/docbook45.rb +158 -163
  15. data/lib/asciidoctor/backends/docbook5.rb +103 -0
  16. data/lib/asciidoctor/backends/html5.rb +662 -445
  17. data/lib/asciidoctor/block.rb +54 -44
  18. data/lib/asciidoctor/cli/invoker.rb +41 -20
  19. data/lib/asciidoctor/cli/options.rb +66 -20
  20. data/lib/asciidoctor/debug.rb +1 -1
  21. data/lib/asciidoctor/document.rb +265 -100
  22. data/lib/asciidoctor/extensions.rb +443 -0
  23. data/lib/asciidoctor/helpers.rb +38 -6
  24. data/lib/asciidoctor/inline.rb +5 -5
  25. data/lib/asciidoctor/lexer.rb +532 -250
  26. data/lib/asciidoctor/{list_item.rb → list.rb} +33 -13
  27. data/lib/asciidoctor/path_resolver.rb +28 -2
  28. data/lib/asciidoctor/reader.rb +814 -455
  29. data/lib/asciidoctor/renderer.rb +128 -42
  30. data/lib/asciidoctor/section.rb +55 -41
  31. data/lib/asciidoctor/substituters.rb +380 -107
  32. data/lib/asciidoctor/table.rb +40 -30
  33. data/lib/asciidoctor/version.rb +1 -1
  34. data/man/asciidoctor.1 +32 -96
  35. data/man/{asciidoctor.ad → asciidoctor.adoc} +57 -48
  36. data/test/attributes_test.rb +200 -27
  37. data/test/blocks_test.rb +361 -22
  38. data/test/document_test.rb +496 -81
  39. data/test/extensions_test.rb +448 -0
  40. data/test/fixtures/basic-docinfo-footer.html +6 -0
  41. data/test/fixtures/basic-docinfo-footer.xml +8 -0
  42. data/test/fixtures/basic-docinfo.xml +3 -3
  43. data/test/fixtures/basic.asciidoc +1 -0
  44. data/test/fixtures/child-include.adoc +5 -0
  45. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
  46. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
  47. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
  48. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
  49. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
  50. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
  51. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
  52. data/test/fixtures/docinfo-footer.html +1 -0
  53. data/test/fixtures/docinfo-footer.xml +9 -0
  54. data/test/fixtures/docinfo.xml +1 -0
  55. data/test/fixtures/grandchild-include.adoc +3 -0
  56. data/test/fixtures/parent-include-restricted.adoc +5 -0
  57. data/test/fixtures/parent-include.adoc +5 -0
  58. data/test/invoker_test.rb +82 -8
  59. data/test/lexer_test.rb +21 -3
  60. data/test/links_test.rb +34 -2
  61. data/test/lists_test.rb +304 -7
  62. data/test/options_test.rb +19 -3
  63. data/test/paragraphs_test.rb +13 -0
  64. data/test/paths_test.rb +22 -0
  65. data/test/preamble_test.rb +20 -0
  66. data/test/reader_test.rb +1096 -644
  67. data/test/renderer_test.rb +152 -12
  68. data/test/sections_test.rb +417 -76
  69. data/test/substitutions_test.rb +339 -138
  70. data/test/tables_test.rb +109 -4
  71. data/test/test_helper.rb +79 -13
  72. data/test/text_test.rb +111 -11
  73. metadata +54 -18
@@ -2,7 +2,15 @@ module Asciidoctor
2
2
  # Public: Methods for rendering Asciidoc Documents, Sections, and Blocks
3
3
  # using eRuby templates.
4
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
+
5
10
  attr_reader :compact
11
+ attr_reader :cache
12
+
13
+ @@global_cache = nil
6
14
 
7
15
  # Public: Initialize an Asciidoctor::Renderer object.
8
16
  #
@@ -11,10 +19,11 @@ class Renderer
11
19
 
12
20
  @views = {}
13
21
  @compact = options[:compact]
22
+ @cache = nil
14
23
 
15
24
  backend = options[:backend]
16
25
  case backend
17
- when 'html5', 'docbook45'
26
+ when 'html5', 'docbook45', 'docbook5'
18
27
  eruby = load_eruby options[:eruby]
19
28
  #Helpers.require_library 'asciidoctor/backends/' + backend
20
29
  require 'asciidoctor/backends/' + backend
@@ -23,7 +32,7 @@ class Renderer
23
32
  if tc.to_s.downcase.include?('::' + backend + '::') # optimization
24
33
  view_name, view_backend = self.class.extract_view_mapping(tc)
25
34
  if view_backend == backend
26
- @views[view_name] = tc.new(view_name, eruby)
35
+ @views[view_name] = tc.new(view_name, backend, eruby)
27
36
  end
28
37
  end
29
38
  end
@@ -32,26 +41,19 @@ class Renderer
32
41
  end
33
42
 
34
43
  # If user passed in a template dir, let them override our base templates
35
- if template_dir = options.delete(:template_dir)
36
- Helpers.require_library 'tilt'
37
-
38
- template_glob = '*'
39
- if (engine = options[:template_engine])
40
- template_glob = "*.#{engine}"
41
- # example: templates/haml
42
- if File.directory? File.join(template_dir, engine)
43
- template_dir = File.join template_dir, engine
44
- end
45
- end
44
+ if (template_dirs = options.delete(:template_dirs))
45
+ Helpers.require_library 'tilt', true
46
46
 
47
- # example: templates/html5 or templates/haml/html5
48
- if File.directory? File.join(template_dir, options[:backend])
49
- template_dir = File.join template_dir, options[:backend]
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
50
52
  end
51
53
 
52
54
  view_opts = {
53
55
  :erb => { :trim => '<>' },
54
- :haml => { :attr_wrapper => '"', :ugly => true, :escape_attrs => false },
56
+ :haml => { :format => :xhtml, :attr_wrapper => '"', :ugly => true, :escape_attrs => false },
55
57
  :slim => { :disable_escape => true, :sort_attrs => false, :pretty => false }
56
58
  }
57
59
 
@@ -61,29 +63,64 @@ class Renderer
61
63
  end
62
64
 
63
65
  slim_loaded = false
64
- helpers = nil
65
-
66
- # Grab the files in the top level of the directory (we're not traversing)
67
- Dir.glob(File.join(template_dir, template_glob)).
68
- select{|f| File.file? f }.each do |template|
69
- basename = File.basename(template)
70
- if basename == 'helpers.rb'
71
- helpers = template
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)
72
89
  next
73
90
  end
74
- name_parts = basename.split('.')
75
- next if name_parts.size < 2
76
- view_name = name_parts.first
77
- ext_name = name_parts.last
78
- if ext_name == 'slim' && !slim_loaded
79
- # slim doesn't get loaded by Tilt
80
- Helpers.require_library 'slim'
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
81
119
  end
82
- next unless Tilt.registered? ext_name
83
- @views[view_name] = Tilt.new(template, nil, view_opts[ext_name.to_sym])
84
- end
85
120
 
86
- require helpers unless helpers.nil?
121
+ require helpers unless helpers.nil?
122
+ @cache.store(scan_result, :scan, template_dir, template_glob) if @cache
123
+ end
87
124
  end
88
125
  end
89
126
 
@@ -106,6 +143,11 @@ class Renderer
106
143
  readonly_views
107
144
  end
108
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
+
109
151
  # Internal: Load the eRuby implementation
110
152
  #
111
153
  # name - the String name of the eRuby implementation (default: 'erb')
@@ -116,15 +158,25 @@ class Renderer
116
158
  name = 'erb'
117
159
  end
118
160
 
119
- Helpers.require_library name
120
-
121
161
  if name == 'erb'
162
+ Helpers.require_library 'erb'
122
163
  ::ERB
123
164
  elsif name == 'erubis'
165
+ Helpers.require_library 'erubis', true
124
166
  ::Erubis::FastEruby
125
167
  end
126
168
  end
127
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
+
128
180
  # Internal: Extracts the view name and backend from a qualified Ruby class
129
181
  #
130
182
  # The purpose of this method is to determine the view name and backend to
@@ -145,8 +197,8 @@ class Renderer
145
197
  # Returns A two-element String Array mapped as [view_name, backend], where backend may be nil
146
198
  def self.extract_view_mapping(qualified_class)
147
199
  view_name, backend = qualified_class.to_s.
148
- gsub(/^Asciidoctor::/, '').
149
- gsub(/Template$/, '').
200
+ sub(RE_ASCIIDOCTOR_NAMESPACE, '').
201
+ sub(RE_TEMPLATE_CLASS_SUFFIX, '').
150
202
  split('::').reverse
151
203
  view_name = camelcase_to_underscore(view_name)
152
204
  backend = backend.downcase unless backend.nil?
@@ -165,9 +217,43 @@ class Renderer
165
217
  #
166
218
  # Returns the String converted from CamelCase to underscore-delimited
167
219
  def self.camelcase_to_underscore(str)
168
- str.gsub(/([[:upper:]]+)([[:upper:]][[:alpha:]])/, '\1_\2').
169
- gsub(/([[:lower:]])([[:upper:]])/, '\1_\2').downcase
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 = {}
170
231
  end
171
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
172
258
  end
173
259
  end
@@ -20,29 +20,41 @@ module Asciidoctor
20
20
  # => 1
21
21
  class Section < AbstractBlock
22
22
 
23
- # Public: Get/Set the Integer index of this section within the parent block
23
+ # Public: Get/Set the 0-based index order of this section within the parent block
24
24
  attr_accessor :index
25
25
 
26
+ # Public: Get/Set the number of this section within the parent block
27
+ # Only relevant if the attribute numbered is true
28
+ attr_accessor :number
29
+
26
30
  # Public: Get/Set the section name of this section
27
31
  attr_accessor :sectname
28
32
 
29
33
  # Public: Get/Set the flag to indicate whether this is a special section or a child of one
30
34
  attr_accessor :special
31
35
 
36
+ # Public: Get the state of the numbered attribute at this section (need to preserve for creating TOC)
37
+ attr_accessor :numbered
38
+
32
39
  # Public: Initialize an Asciidoctor::Section object.
33
40
  #
34
41
  # parent - The parent Asciidoc Object.
35
- def initialize(parent = nil, level = nil)
42
+ def initialize(parent = nil, level = nil, numbered = true)
36
43
  super(parent, :section)
37
- if level.nil? && !parent.nil?
38
- @level = parent.level + 1
39
- end
40
- if parent.is_a?(Section) && parent.special
41
- @special = true
44
+ @template_name = 'section'
45
+ if level.nil?
46
+ if !parent.nil?
47
+ @level = parent.level + 1
48
+ elsif @level.nil?
49
+ @level = 1
50
+ end
42
51
  else
43
- @special = false
52
+ @level = level
44
53
  end
54
+ @numbered = numbered && @level > 0 && @level < 4
55
+ @special = parent.is_a?(Section) && parent.special
45
56
  @index = 0
57
+ @number = 1
46
58
  end
47
59
 
48
60
  # Public: The name of this section, an alias of the section title
@@ -69,16 +81,25 @@ class Section < AbstractBlock
69
81
  # another_section.title = "Foo"
70
82
  # another_section.generate_id
71
83
  # => "_foo_1"
84
+ #
85
+ # yet_another_section = Section.new(parent)
86
+ # yet_another_section.title = "Ben & Jerry"
87
+ # yet_another_section.generate_id
88
+ # => "_ben_jerry"
72
89
  def generate_id
73
- if @document.attr? 'sectids'
74
- separator = @document.attr('idseparator', '_')
75
- # FIXME define constants for these regexps
76
- base_id = @document.attr('idprefix', '_') + title.downcase.gsub(/&#[0-9]+;/, separator).
77
- gsub(/\W+/, separator).tr_s(separator, separator).chomp(separator)
90
+ if @document.attributes.has_key? 'sectids'
91
+ sep = @document.attributes['idseparator'] || '_'
92
+ pre = @document.attributes['idprefix'] || '_'
93
+ base_id = %(#{pre}#{title.downcase.gsub(REGEXP[:illegal_sectid_chars], sep).tr_s(sep, sep).chomp(sep)})
94
+ # ensure id doesn't begin with idprefix if requested it doesn't
95
+ if pre.empty? && base_id.start_with?(sep)
96
+ base_id = base_id[1..-1]
97
+ base_id = base_id[1..-1] while base_id.start_with?(sep)
98
+ end
78
99
  gen_id = base_id
79
100
  cnt = 2
80
- while @document.references[:ids].has_key? gen_id
81
- gen_id = "#{base_id}#{separator}#{cnt}"
101
+ while @document.references[:ids].has_key? gen_id
102
+ gen_id = "#{base_id}#{sep}#{cnt}"
82
103
  cnt += 1
83
104
  end
84
105
  gen_id
@@ -87,33 +108,12 @@ class Section < AbstractBlock
87
108
  end
88
109
  end
89
110
 
90
- # Public: Get the rendered String content for this Section and all its child
91
- # Blocks.
92
- def render
93
- @document.playback_attributes @attributes
94
- renderer.render('section', self)
95
- end
96
-
97
- # Public: Get the String section content by aggregating rendered section blocks.
98
- #
99
- # Examples
100
- #
101
- # section = Section.new
102
- # section << 'foo'
103
- # section << 'bar'
104
- # section << 'baz'
105
- # section.content
106
- # "<div class=\"paragraph\"><p>foo</p></div>\n<div class=\"paragraph\"><p>bar</p></div>\n<div class=\"paragraph\"><p>baz</p></div>"
107
- def content
108
- @blocks.map {|b| b.render }.join
109
- end
110
-
111
111
  # Public: Get the section number for the current Section
112
112
  #
113
113
  # The section number is a unique, dot separated String
114
114
  # where each entry represents one level of nesting and
115
- # the value of each entry is the 1-based index of
116
- # the Section amongst its sibling Sections
115
+ # the value of each entry is the 1-based outline number
116
+ # of the Section amongst its numbered sibling Sections
117
117
  #
118
118
  # delimiter - the delimiter to separate the number for each level
119
119
  # append - the String to append at the end of the section number
@@ -154,15 +154,29 @@ class Section < AbstractBlock
154
154
  def sectnum(delimiter = '.', append = nil)
155
155
  append ||= (append == false ? '' : delimiter)
156
156
  if !@level.nil? && @level > 1 && @parent.is_a?(Section)
157
- "#{@parent.sectnum(delimiter)}#{@index + 1}#{append}"
157
+ "#{@parent.sectnum(delimiter)}#{@number}#{append}"
158
158
  else
159
- "#{@index + 1}#{append}"
159
+ "#{@number}#{append}"
160
+ end
161
+ end
162
+
163
+ # Public: Append a content block to this block's list of blocks.
164
+ #
165
+ # If the child block is a Section, assign an index to it.
166
+ #
167
+ # block - The child Block to append to this parent Block
168
+ #
169
+ # Returns nothing.
170
+ def <<(block)
171
+ super
172
+ if block.context == :section
173
+ assign_index block
160
174
  end
161
175
  end
162
176
 
163
177
  def to_s
164
178
  if @title
165
- if @level && @index
179
+ if @numbered
166
180
  %[#{super.to_s} - #{sectnum} #@title [blocks:#{@blocks.size}]]
167
181
  else
168
182
  %[#{super.to_s} - #@title [blocks:#{@blocks.size}]]
@@ -4,47 +4,68 @@ module Asciidoctor
4
4
  # the necessary substitutions.
5
5
  module Substituters
6
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
+
7
16
  COMPOSITE_SUBS = {
8
17
  :none => [],
9
- :normal => [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements],
10
- :verbatim => [:specialcharacters, :callouts]
18
+ :normal => SUBS[:normal],
19
+ :verbatim => SUBS[:verbatim]
11
20
  }
12
21
 
13
- SUB_OPTIONS = COMPOSITE_SUBS.keys + COMPOSITE_SUBS[:normal]
22
+ SUB_OPTIONS = {
23
+ :block => COMPOSITE_SUBS.keys + SUBS[:normal] + [:callouts],
24
+ :inline => COMPOSITE_SUBS.keys + SUBS[:normal]
25
+ }
14
26
 
15
27
  # Internal: A String Array of passthough (unprocessed) text captured from this block
16
28
  attr_reader :passthroughs
17
29
 
18
30
  # Public: Apply the specified substitutions to the lines of text
19
31
  #
20
- # lines - The lines of text to process. Can be a String or a String Array
21
- # subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default: COMPOSITE_SUBS[:normal])
22
- #
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
+ #
23
36
  # returns Either a String or String Array, whichever matches the type of the first argument
24
- def apply_subs(lines, subs = COMPOSITE_SUBS[:normal])
25
- if subs.nil?
26
- subs = []
27
- elsif subs.is_a? Symbol
28
- subs = [subs]
29
- end
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
30
54
 
31
- if !subs.empty?
32
- # QUESTION is this most efficient operation?
33
- subs = subs.map {|key|
34
- COMPOSITE_SUBS.has_key?(key) ? COMPOSITE_SUBS[key] : key
35
- }.flatten
55
+ subs = effective_subs
56
+ end
36
57
  end
37
58
 
38
- return lines if subs.empty?
59
+ return source if subs.empty?
39
60
 
40
- multiline = lines.is_a?(Array)
41
- text = multiline ? lines.join : lines
61
+ multiline = source.is_a?(Array)
62
+ text = multiline ? source.join : source
42
63
 
43
64
  if (has_passthroughs = subs.include?(:macros))
44
65
  text = extract_passthroughs(text)
45
66
  end
46
-
47
- subs.each {|type|
67
+
68
+ subs.each do |type|
48
69
  case type
49
70
  when :specialcharacters
50
71
  text = sub_specialcharacters(text)
@@ -56,14 +77,16 @@ module Substituters
56
77
  text = sub_replacements(text)
57
78
  when :macros
58
79
  text = sub_macros(text)
80
+ when :highlight
81
+ text = highlight_source(text, subs.include?(:callouts))
59
82
  when :callouts
60
- text = sub_callouts(text)
83
+ text = sub_callouts(text) unless subs.include?(:highlight)
61
84
  when :post_replacements
62
85
  text = sub_post_replacements(text)
63
86
  else
64
- puts "asciidoctor: WARNING: unknown substitution type #{type}"
87
+ warn "asciidoctor: WARNING: unknown substitution type #{type}"
65
88
  end
66
- }
89
+ end
67
90
  text = restore_passthroughs(text) if has_passthroughs
68
91
 
69
92
  multiline ? text.lines.entries : text
@@ -71,11 +94,11 @@ module Substituters
71
94
 
72
95
  # Public: Apply normal substitutions.
73
96
  #
74
- # lines - The lines of text to process. Can be a String or a String Array
97
+ # lines - The lines of text to process. Can be a String or a String Array
75
98
  #
76
99
  # returns - A String with normal substitutions performed
77
100
  def apply_normal_subs(lines)
78
- apply_subs(lines.is_a?(Array) ? lines.join : lines)
101
+ apply_subs lines.is_a?(Array) ? lines.join : lines
79
102
  end
80
103
 
81
104
  # Public: Apply substitutions for titles.
@@ -84,23 +107,7 @@ module Substituters
84
107
  #
85
108
  # returns - A String with title substitutions performed
86
109
  def apply_title_subs(title)
87
- apply_subs(title, [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements])
88
- end
89
-
90
- # Public: Apply substitutions for titles
91
- #
92
- # lines - A String Array containing the lines of text process
93
- #
94
- # returns - A String with literal (verbatim) substitutions performed
95
- def apply_literal_subs(lines)
96
- if attr? 'subs'
97
- apply_subs(lines.join, resolve_subs(attr 'subs'))
98
- elsif @document.attributes['basebackend'] == 'html' && attr('style') == 'source' &&
99
- @document.attributes['source-highlighter'] == 'coderay' && attr?('language')
100
- sub_callouts(highlight_source(lines.join))
101
- else
102
- apply_subs(lines.join, COMPOSITE_SUBS[:verbatim])
103
- end
110
+ apply_subs title, SUBS[:title]
104
111
  end
105
112
 
106
113
  # Public: Apply substitutions for header metadata and attribute assignments
@@ -109,19 +116,37 @@ module Substituters
109
116
  #
110
117
  # returns - A String with header substitutions performed
111
118
  def apply_header_subs(text)
112
- apply_subs(text, [:specialcharacters, :attributes])
119
+ apply_subs text, SUBS[:header]
113
120
  end
114
121
 
122
+ =begin
115
123
  # Public: Apply explicit substitutions, if specified, otherwise normal substitutions.
116
124
  #
117
- # lines - The lines of text to process. Can be a String or a String Array
125
+ # lines - The lines of text to process. Can be a String or a String Array
118
126
  #
119
127
  # returns - A String with substitutions applied
120
128
  def apply_para_subs(lines)
121
- if attr? 'subs'
122
- apply_subs(lines.join, resolve_subs(attr 'subs'))
129
+ if (subs = attr('subs', nil, false))
130
+ apply_subs lines.join, resolve_subs(subs)
123
131
  else
124
- apply_subs(lines.join)
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]
125
150
  end
126
151
  end
127
152
 
@@ -131,13 +156,14 @@ module Substituters
131
156
  #
132
157
  # returns - A String with passthrough substitutions performed
133
158
  def apply_passthrough_subs(lines)
134
- if attr? 'subs'
135
- subs = resolve_subs(attr('subs'))
159
+ if (subs = attr('subs', nil, false))
160
+ subs = resolve_subs(subs)
136
161
  else
137
- subs = [:attributes, :macros]
162
+ subs = SUBS[:pass]
138
163
  end
139
- apply_subs(lines.join, subs)
164
+ apply_subs lines.join, subs
140
165
  end
166
+ =end
141
167
 
142
168
  # Internal: Extract the passthrough text from the document for reinsertion after processing.
143
169
  #
@@ -157,10 +183,10 @@ module Substituters
157
183
 
158
184
  if m[1] == '$$'
159
185
  subs = [:specialcharacters]
160
- elsif !m[3].nil? && !m[3].empty?
161
- subs = resolve_subs(m[3])
162
- else
186
+ elsif m[3].nil? || m[3].empty?
163
187
  subs = []
188
+ else
189
+ subs = resolve_pass_subs m[3]
164
190
  end
165
191
 
166
192
  # TODO move unescaping closing square bracket to an operation
@@ -173,14 +199,23 @@ module Substituters
173
199
  # alias match for Ruby 1.8.7 compat
174
200
  m = $~
175
201
 
202
+ unescaped_attrs = nil
176
203
  # honor the escape
177
- if m[2].start_with? '\\'
178
- next "#{m[1]}#{m[2][1..-1]}"
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 = {}
179
214
  end
180
-
181
- @passthroughs << {:text => m[3], :subs => [:specialcharacters], :literal => true}
215
+
216
+ @passthroughs << {:text => m[4], :subs => [:specialcharacters], :attributes => attributes, :literal => true}
182
217
  index = @passthroughs.size - 1
183
- "#{m[1]}\e#{index}\e"
218
+ "#{unescaped_attrs || m[1]}\e#{index}\e"
184
219
  } unless !result.include?('`')
185
220
 
186
221
  result
@@ -193,11 +228,11 @@ module Substituters
193
228
  # returns The String text with the passthrough text restored
194
229
  def restore_passthroughs(text)
195
230
  return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\e")
196
-
231
+
197
232
  text.gsub(REGEXP[:pass_placeholder]) {
198
233
  pass = @passthroughs[$1.to_i];
199
234
  text = apply_subs(pass[:text], pass.fetch(:subs, []))
200
- pass[:literal] ? Inline.new(self, :quoted, text, :type => :monospaced).render : text
235
+ pass[:literal] ? Inline.new(self, :quoted, text, :type => :monospaced, :attributes => pass.fetch(:attributes, {})).render : text
201
236
  }
202
237
  end
203
238
 
@@ -214,6 +249,7 @@ module Substituters
214
249
 
215
250
  text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] }
216
251
  end
252
+ alias :sub_specialchars :sub_specialcharacters
217
253
 
218
254
  # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc)
219
255
  #
@@ -226,7 +262,7 @@ module Substituters
226
262
  QUOTE_SUBS.each {|type, scope, pattern|
227
263
  result.gsub!(pattern) { transform_quoted_text($~, type, scope) }
228
264
  }
229
-
265
+
230
266
  result
231
267
  end
232
268
 
@@ -252,12 +288,12 @@ module Substituters
252
288
  when :leading
253
289
  "#{head}#{replacement}"
254
290
  when :bounding
255
- "#{head}#{replacement}#{tail}"
291
+ "#{head}#{replacement}#{tail}"
256
292
  end
257
293
  end
258
294
  }
259
295
  }
260
-
296
+
261
297
  result
262
298
  end
263
299
 
@@ -273,7 +309,7 @@ module Substituters
273
309
  #--
274
310
  # NOTE it's necessary to perform this substitution line-by-line
275
311
  # so that a missing key doesn't wipe out the whole block of data
276
- def sub_attributes(data)
312
+ def sub_attributes(data, opts = {})
277
313
  return data if data.nil? || data.empty?
278
314
 
279
315
  string_data = data.is_a? String
@@ -297,8 +333,11 @@ module Substituters
297
333
  args = expr.split(':')
298
334
  _, value = Lexer::store_attribute(args[0], args[1] || '', @document)
299
335
  if value.nil?
300
- reject = true
301
- break '{undefined}'
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
302
341
  end
303
342
  ''
304
343
  when 'counter', 'counter2'
@@ -307,7 +346,7 @@ module Substituters
307
346
  directive == 'counter2' ? '' : val
308
347
  else
309
348
  # if we get here, our attr_ref regex is too loose
310
- puts "asciidoctor: WARNING: illegal attribute directive: #{m[2]}"
349
+ warn "asciidoctor: WARNING: illegal attribute directive: #{m[2]}"
311
350
  ''
312
351
  end
313
352
  elsif (key = m[2].downcase) && @document.attributes.has_key?(key)
@@ -315,11 +354,17 @@ module Substituters
315
354
  elsif INTRINSICS.has_key? key
316
355
  INTRINSICS[key]
317
356
  else
318
- Debug.debug { "Missing attribute: #{key}, line marked for removal" }
319
- reject = true
320
- break '{undefined}'
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
321
366
  end
322
- } if line.include? '{'
367
+ } if line.include? '{'
323
368
 
324
369
  result << line unless reject
325
370
  }
@@ -378,14 +423,14 @@ module Substituters
378
423
  c
379
424
  }
380
425
  end
381
- Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).render
426
+ Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).render
382
427
  elsif captured.start_with?('btn')
383
428
  label = unescape_bracketed_text m[1]
384
429
  Inline.new(self, :button, label).render
385
430
  end
386
431
  }
387
432
  end
388
-
433
+
389
434
  if found[:macroish] && result.include?('menu:')
390
435
  result.gsub!(REGEXP[:menu_macro]) {
391
436
  # alias match for Ruby 1.8.7 compat
@@ -427,13 +472,37 @@ module Substituters
427
472
  input = m[1]
428
473
 
429
474
  menu, *submenus = input.split('&gt;').map(&:strip)
430
- menuitem = submenus.pop
475
+ menuitem = submenus.pop
431
476
  Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render
432
477
  }
433
478
  end
434
479
  end
435
480
 
436
- if found[:macroish] && result.include?('image:')
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:'))
437
506
  # image:filename.png[Alt Text]
438
507
  result.gsub!(REGEXP[:image_macro]) {
439
508
  # alias match for Ruby 1.8.7 compat
@@ -442,13 +511,24 @@ module Substituters
442
511
  if m[0].start_with? '\\'
443
512
  next m[0][1..-1]
444
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
445
523
  target = sub_attributes(m[1])
446
- @document.register(:images, target)
447
- attrs = parse_attributes(unescape_bracketed_text(m[2]), ['alt', 'width', 'height'])
524
+ unless type == 'icon'
525
+ @document.register(:images, target)
526
+ end
527
+ attrs = parse_attributes(raw_attrs, posattrs)
448
528
  if !attrs['alt']
449
529
  attrs['alt'] = File.basename(target, File.extname(target))
450
530
  end
451
- Inline.new(self, :image, nil, :target => target, :attributes => attrs).render
531
+ Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).render
452
532
  }
453
533
  end
454
534
 
@@ -464,10 +544,10 @@ module Substituters
464
544
  end
465
545
 
466
546
  terms = unescape_bracketed_text(m[1] || m[2]).split(',').map(&:strip)
467
- document.register(:indexterms, [*terms])
547
+ @document.register(:indexterms, [*terms])
468
548
  Inline.new(self, :indexterm, text, :attributes => {'terms' => terms}).render
469
549
  }
470
-
550
+
471
551
  # indexterm2:[Tigers]
472
552
  # ((Tigers))
473
553
  result.gsub!(REGEXP[:indexterm2_macro]) {
@@ -479,7 +559,7 @@ module Substituters
479
559
  end
480
560
 
481
561
  text = unescape_bracketed_text(m[1] || m[2])
482
- document.register(:indexterms, [text])
562
+ @document.register(:indexterms, [text])
483
563
  Inline.new(self, :indexterm, text, :type => :visible).render
484
564
  }
485
565
  end
@@ -654,7 +734,33 @@ module Substituters
654
734
  id = m[2]
655
735
  reftext = !m[3].empty? ? m[3] : nil
656
736
  end
657
- Inline.new(self, :anchor, reftext, :type => :xref, :target => id).render
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
658
764
  }
659
765
  end
660
766
 
@@ -707,10 +813,11 @@ module Substituters
707
813
  # alias match for Ruby 1.8.7 compat
708
814
  m = $~
709
815
  # honor the escape
710
- if m[0].start_with? '\\'
711
- next "&lt;#{m[1]}&gt;"
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('\\', '')
712
819
  end
713
- Inline.new(self, :callout, m[1], :id => document.callouts.read_next_id).render
820
+ Inline.new(self, :callout, m[3], :id => @document.callouts.read_next_id).render
714
821
  }
715
822
  end
716
823
 
@@ -720,11 +827,11 @@ module Substituters
720
827
  #
721
828
  # returns The String with the post replacements rendered using the backend templates
722
829
  def sub_post_replacements(text)
723
- if @document.attr? 'hardbreaks'
830
+ if @document.attributes['hardbreaks']
724
831
  lines = text.lines.entries
725
832
  return text if lines.size == 1
726
833
  last = lines.pop
727
- "#{lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).render } * "\n"}\n#{last}"
834
+ lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).render }.push(last) * EOL
728
835
  else
729
836
  text.gsub(REGEXP[:line_break]) { Inline.new(self, :break, $1, :type => :line).render }
730
837
  end
@@ -738,12 +845,70 @@ module Substituters
738
845
  #
739
846
  # returns The rendered text for the quoted text region
740
847
  def transform_quoted_text(match, type, scope)
848
+ unescaped_attrs = nil
741
849
  if match[0].start_with? '\\'
742
- match[0][1..-1]
743
- elsif scope == :constrained
744
- "#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :attributes => parse_attributes(match[2])).render}"
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
745
865
  else
746
- Inline.new(self, :quoted, match[2], :type => type, :attributes => parse_attributes(match[1])).render
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}
747
912
  end
748
913
  end
749
914
 
@@ -763,7 +928,7 @@ module Substituters
763
928
  # substitutions are only performed on attribute values if block is not nil
764
929
  block = self
765
930
  end
766
-
931
+
767
932
  if opts.has_key?(:into)
768
933
  AttributeList.new(attrline, block).parse_into(opts[:into], posattrs)
769
934
  else
@@ -775,7 +940,7 @@ module Substituters
775
940
  # square brackets from text extracted from brackets
776
941
  def unescape_bracketed_text(text)
777
942
  return '' if text.empty?
778
- text.strip.tr("\n", ' ').gsub('\]', ']')
943
+ text.strip.tr(EOL, ' ').gsub('\]', ']')
779
944
  end
780
945
 
781
946
  # Internal: Resolve the list of comma-delimited subs against the possible options.
@@ -783,28 +948,136 @@ module Substituters
783
948
  # subs - A comma-delimited String of substitution aliases
784
949
  #
785
950
  # returns An Array of Symbols representing the substitution operation
786
- def resolve_subs(subs)
787
- candidates = subs.split(',').map {|sub| sub.strip.to_sym}
788
- resolved = candidates & SUB_OPTIONS
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]
789
967
  if (invalid = candidates - resolved).size > 0
790
- puts "asciidoctor: WARNING: invalid passthrough macro substitution operation#{invalid.size > 1 ? 's' : ''}: #{invalid * ', '}"
791
- end
968
+ warn "asciidoctor: WARNING: invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : nil}#{subject}: #{invalid * ', '}"
969
+ end
792
970
  resolved
793
971
  end
794
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
+
795
981
  # Public: Highlight the source code if a source highlighter is defined
796
982
  # on the document, otherwise return the text unprocessed
797
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
+ #
798
988
  # source - the source code String to highlight
989
+ # sub_callouts - a Boolean flag indicating whether callout marks should be substituted
799
990
  #
800
991
  # returns the highlighted source code, if a source highlighter is defined
801
992
  # on the document, otherwise the unprocessed text
802
- def highlight_source(source)
803
- Helpers.require_library 'coderay'
804
- ::CodeRay::Duo[attr('language', 'text').to_sym, :html, {
805
- :css => @document.attributes.fetch('coderay-css', 'class').to_sym,
806
- :line_numbers => (attr?('linenums') ? @document.attributes.fetch('coderay-linenums-mode', 'table').to_sym : nil),
807
- :line_number_anchors => false}].highlight(source).chomp
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
808
1081
  end
809
1082
  end
810
1083
  end