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.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +387 -0
- data/README.adoc +358 -348
- data/asciidoctor.gemspec +30 -9
- data/bin/asciidoctor +3 -0
- data/bin/asciidoctor-safe +3 -0
- data/compat/asciidoc.conf +76 -4
- data/lib/asciidoctor.rb +174 -79
- data/lib/asciidoctor/abstract_block.rb +131 -101
- data/lib/asciidoctor/abstract_node.rb +108 -26
- data/lib/asciidoctor/attribute_list.rb +1 -1
- data/lib/asciidoctor/backends/_stylesheets.rb +204 -62
- data/lib/asciidoctor/backends/base_template.rb +11 -22
- data/lib/asciidoctor/backends/docbook45.rb +158 -163
- data/lib/asciidoctor/backends/docbook5.rb +103 -0
- data/lib/asciidoctor/backends/html5.rb +662 -445
- data/lib/asciidoctor/block.rb +54 -44
- data/lib/asciidoctor/cli/invoker.rb +41 -20
- data/lib/asciidoctor/cli/options.rb +66 -20
- data/lib/asciidoctor/debug.rb +1 -1
- data/lib/asciidoctor/document.rb +265 -100
- data/lib/asciidoctor/extensions.rb +443 -0
- data/lib/asciidoctor/helpers.rb +38 -6
- data/lib/asciidoctor/inline.rb +5 -5
- data/lib/asciidoctor/lexer.rb +532 -250
- data/lib/asciidoctor/{list_item.rb → list.rb} +33 -13
- data/lib/asciidoctor/path_resolver.rb +28 -2
- data/lib/asciidoctor/reader.rb +814 -455
- data/lib/asciidoctor/renderer.rb +128 -42
- data/lib/asciidoctor/section.rb +55 -41
- data/lib/asciidoctor/substituters.rb +380 -107
- data/lib/asciidoctor/table.rb +40 -30
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +32 -96
- data/man/{asciidoctor.ad → asciidoctor.adoc} +57 -48
- data/test/attributes_test.rb +200 -27
- data/test/blocks_test.rb +361 -22
- data/test/document_test.rb +496 -81
- data/test/extensions_test.rb +448 -0
- data/test/fixtures/basic-docinfo-footer.html +6 -0
- data/test/fixtures/basic-docinfo-footer.xml +8 -0
- data/test/fixtures/basic-docinfo.xml +3 -3
- data/test/fixtures/basic.asciidoc +1 -0
- data/test/fixtures/child-include.adoc +5 -0
- data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
- data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
- data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
- data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
- data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
- data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
- data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
- data/test/fixtures/docinfo-footer.html +1 -0
- data/test/fixtures/docinfo-footer.xml +9 -0
- data/test/fixtures/docinfo.xml +1 -0
- data/test/fixtures/grandchild-include.adoc +3 -0
- data/test/fixtures/parent-include-restricted.adoc +5 -0
- data/test/fixtures/parent-include.adoc +5 -0
- data/test/invoker_test.rb +82 -8
- data/test/lexer_test.rb +21 -3
- data/test/links_test.rb +34 -2
- data/test/lists_test.rb +304 -7
- data/test/options_test.rb +19 -3
- data/test/paragraphs_test.rb +13 -0
- data/test/paths_test.rb +22 -0
- data/test/preamble_test.rb +20 -0
- data/test/reader_test.rb +1096 -644
- data/test/renderer_test.rb +152 -12
- data/test/sections_test.rb +417 -76
- data/test/substitutions_test.rb +339 -138
- data/test/tables_test.rb +109 -4
- data/test/test_helper.rb +79 -13
- data/test/text_test.rb +111 -11
- metadata +54 -18
data/lib/asciidoctor/renderer.rb
CHANGED
@@ -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
|
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
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
149
|
-
|
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(
|
169
|
-
gsub(
|
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
|
data/lib/asciidoctor/section.rb
CHANGED
@@ -20,29 +20,41 @@ module Asciidoctor
|
|
20
20
|
# => 1
|
21
21
|
class Section < AbstractBlock
|
22
22
|
|
23
|
-
# Public: Get/Set the
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@
|
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
|
-
@
|
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.
|
74
|
-
|
75
|
-
|
76
|
-
base_id =
|
77
|
-
|
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}#{
|
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
|
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)}#{@
|
157
|
+
"#{@parent.sectnum(delimiter)}#{@number}#{append}"
|
158
158
|
else
|
159
|
-
"#{@
|
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 @
|
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 => [:
|
10
|
-
:verbatim => [:
|
18
|
+
:normal => SUBS[:normal],
|
19
|
+
:verbatim => SUBS[:verbatim]
|
11
20
|
}
|
12
21
|
|
13
|
-
SUB_OPTIONS =
|
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
|
-
#
|
21
|
-
# subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default:
|
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
|
25
|
-
if subs
|
26
|
-
subs = []
|
27
|
-
elsif subs.
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
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
|
59
|
+
return source if subs.empty?
|
39
60
|
|
40
|
-
multiline =
|
41
|
-
text = multiline ?
|
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
|
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
|
-
|
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
|
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
|
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
|
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
|
122
|
-
apply_subs
|
129
|
+
if (subs = attr('subs', nil, false))
|
130
|
+
apply_subs lines.join, resolve_subs(subs)
|
123
131
|
else
|
124
|
-
apply_subs
|
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
|
135
|
-
subs = resolve_subs(
|
159
|
+
if (subs = attr('subs', nil, false))
|
160
|
+
subs = resolve_subs(subs)
|
136
161
|
else
|
137
|
-
subs = [:
|
162
|
+
subs = SUBS[:pass]
|
138
163
|
end
|
139
|
-
apply_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
|
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[
|
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[
|
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
|
-
|
301
|
-
|
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
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
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('>').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
|
-
|
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
|
-
|
447
|
-
|
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
|
-
|
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[
|
711
|
-
|
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[
|
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.
|
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
|
-
|
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[
|
743
|
-
|
744
|
-
|
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
|
-
|
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(
|
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
|
787
|
-
|
788
|
-
|
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
|
-
|
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
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
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
|