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