asciidoctor 1.5.6.2 → 1.5.7

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of asciidoctor might be problematic. Click here for more details.

Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +330 -143
  3. data/README-fr.adoc +441 -0
  4. data/README-jp.adoc +418 -0
  5. data/README-zh_CN.adoc +430 -0
  6. data/README.adoc +454 -0
  7. data/Rakefile +57 -0
  8. data/asciidoctor.gemspec +7 -1
  9. data/data/locale/attributes-ar.adoc +22 -0
  10. data/data/locale/attributes-bg.adoc +22 -0
  11. data/data/locale/attributes-ca.adoc +22 -0
  12. data/data/locale/attributes-cs.adoc +22 -0
  13. data/data/locale/attributes-da.adoc +22 -0
  14. data/data/locale/attributes-de.adoc +22 -0
  15. data/data/locale/attributes-en.adoc +23 -0
  16. data/data/locale/attributes-es.adoc +22 -0
  17. data/data/locale/attributes-fa.adoc +22 -0
  18. data/data/locale/attributes-fi.adoc +22 -0
  19. data/data/locale/attributes-fr.adoc +22 -0
  20. data/data/locale/attributes-hu.adoc +22 -0
  21. data/data/locale/attributes-id.adoc +22 -0
  22. data/data/locale/attributes-it.adoc +22 -0
  23. data/data/locale/attributes-ja.adoc +22 -0
  24. data/data/locale/attributes-kr.adoc +22 -0
  25. data/data/locale/attributes-nb.adoc +22 -0
  26. data/data/locale/attributes-nl.adoc +22 -0
  27. data/data/locale/attributes-nn.adoc +22 -0
  28. data/data/locale/attributes-pl.adoc +22 -0
  29. data/data/locale/attributes-pt.adoc +22 -0
  30. data/data/locale/attributes-pt_BR.adoc +22 -0
  31. data/data/locale/attributes-ro.adoc +22 -0
  32. data/data/locale/attributes-ru.adoc +22 -0
  33. data/data/locale/attributes-sr.adoc +22 -0
  34. data/data/locale/attributes-sr_Latn.adoc +22 -0
  35. data/data/locale/attributes-tr.adoc +22 -0
  36. data/data/locale/attributes-uk.adoc +22 -0
  37. data/data/locale/attributes-zh_CN.adoc +22 -0
  38. data/data/locale/attributes-zh_TW.adoc +22 -0
  39. data/data/locale/attributes.adoc +8 -649
  40. data/data/stylesheets/asciidoctor-default.css +77 -72
  41. data/features/xref.feature +366 -7
  42. data/lib/asciidoctor.rb +107 -93
  43. data/lib/asciidoctor/abstract_block.rb +247 -239
  44. data/lib/asciidoctor/abstract_node.rb +56 -58
  45. data/lib/asciidoctor/block.rb +3 -3
  46. data/lib/asciidoctor/callouts.rb +1 -1
  47. data/lib/asciidoctor/cli/invoker.rb +36 -9
  48. data/lib/asciidoctor/cli/options.rb +63 -25
  49. data/lib/asciidoctor/converter.rb +23 -13
  50. data/lib/asciidoctor/converter/base.rb +4 -0
  51. data/lib/asciidoctor/converter/docbook45.rb +16 -9
  52. data/lib/asciidoctor/converter/docbook5.rb +115 -97
  53. data/lib/asciidoctor/converter/factory.rb +29 -31
  54. data/lib/asciidoctor/converter/html5.rb +229 -192
  55. data/lib/asciidoctor/converter/manpage.rb +72 -50
  56. data/lib/asciidoctor/converter/template.rb +12 -12
  57. data/lib/asciidoctor/core_ext.rb +5 -1
  58. data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +6 -0
  59. data/lib/asciidoctor/document.rb +168 -77
  60. data/lib/asciidoctor/extensions.rb +79 -47
  61. data/lib/asciidoctor/helpers.rb +33 -11
  62. data/lib/asciidoctor/inline.rb +3 -2
  63. data/lib/asciidoctor/list.rb +2 -1
  64. data/lib/asciidoctor/logging.rb +122 -0
  65. data/lib/asciidoctor/parser.rb +406 -382
  66. data/lib/asciidoctor/path_resolver.rb +169 -162
  67. data/lib/asciidoctor/reader.rb +166 -121
  68. data/lib/asciidoctor/section.rb +45 -28
  69. data/lib/asciidoctor/stylesheets.rb +13 -5
  70. data/lib/asciidoctor/substitutors.rb +328 -254
  71. data/lib/asciidoctor/table.rb +105 -48
  72. data/lib/asciidoctor/timings.rb +34 -6
  73. data/lib/asciidoctor/version.rb +1 -1
  74. data/man/asciidoctor.1 +41 -23
  75. data/man/asciidoctor.adoc +14 -8
  76. data/test/api_test.rb +1004 -0
  77. data/test/attributes_test.rb +241 -50
  78. data/test/blocks_test.rb +549 -124
  79. data/test/converter_test.rb +170 -78
  80. data/test/document_test.rb +208 -767
  81. data/test/extensions_test.rb +188 -53
  82. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +1 -1
  83. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +1 -1
  84. data/test/fixtures/file-with-missing-include.adoc +1 -0
  85. data/test/fixtures/include-file.jsx +8 -0
  86. data/test/fixtures/lists.adoc +96 -0
  87. data/test/fixtures/other-chapters.adoc +11 -0
  88. data/test/fixtures/outer-include.adoc +5 -0
  89. data/test/fixtures/sample.asciidoc +5 -1
  90. data/test/fixtures/subdir/index.adoc +3 -0
  91. data/test/fixtures/subdir/inner-include.adoc +3 -0
  92. data/test/fixtures/subdir/middle-include.adoc +5 -0
  93. data/test/fixtures/tagged-class-enclosed.rb +0 -1
  94. data/test/fixtures/unclosed-tag.adoc +3 -0
  95. data/test/fixtures/unexpected-end-tag.adoc +4 -0
  96. data/test/invoker_test.rb +101 -40
  97. data/test/links_test.rb +266 -72
  98. data/test/lists_test.rb +243 -45
  99. data/test/logger_test.rb +211 -0
  100. data/test/manpage_test.rb +124 -6
  101. data/test/options_test.rb +46 -1
  102. data/test/paragraphs_test.rb +23 -10
  103. data/test/parser_test.rb +30 -1
  104. data/test/paths_test.rb +115 -33
  105. data/test/preamble_test.rb +1 -1
  106. data/test/reader_test.rb +337 -81
  107. data/test/sections_test.rb +656 -72
  108. data/test/substitutions_test.rb +182 -57
  109. data/test/tables_test.rb +324 -57
  110. data/test/test_helper.rb +77 -32
  111. data/test/text_test.rb +7 -7
  112. metadata +67 -3
@@ -38,14 +38,19 @@ class Section < AbstractBlock
38
38
 
39
39
  # Public: Initialize an Asciidoctor::Section object.
40
40
  #
41
- # parent - The parent Asciidoc Object.
42
- def initialize parent = nil, level = nil, numbered = true, opts = {}
41
+ # parent - The parent AbstractBlock. If set, must be a Document or Section object (default: nil)
42
+ # level - The Integer level of this section (default: 1 more than parent level or 1 if parent not defined)
43
+ # numbered - A Boolean indicating whether numbering is enabled for this Section (default: false)
44
+ # opts - An optional Hash of options (default: {})
45
+ def initialize parent = nil, level = nil, numbered = false, opts = {}
43
46
  super parent, :section, opts
44
- @level = level ? level : (parent ? (parent.level + 1) : 1)
45
- @numbered = numbered && @level > 0
46
- @special = parent && parent.context == :section && parent.special
47
+ if Section === parent
48
+ @level, @special = level || (parent.level + 1), parent.special
49
+ else
50
+ @level, @special = level || 1, false
51
+ end
52
+ @numbered = numbered
47
53
  @index = 0
48
- @number = 1
49
54
  end
50
55
 
51
56
  # Public: The name of this section, an alias of the section title
@@ -60,10 +65,12 @@ class Section < AbstractBlock
60
65
 
61
66
  # Public: Get the section number for the current Section
62
67
  #
63
- # The section number is a unique, dot separated String
64
- # where each entry represents one level of nesting and
65
- # the value of each entry is the 1-based outline number
66
- # of the Section amongst its numbered sibling Sections
68
+ # The section number is a dot-separated String that uniquely describes the position of this
69
+ # Section in the document. Each entry represents a level of nesting. The value of each entry is
70
+ # the 1-based outline number of the Section amongst its numbered sibling Sections.
71
+ #
72
+ # This method assumes that both the @level and @parent instance variables have been assigned.
73
+ # The method also assumes that the value of @parent is either a Document or Section.
67
74
  #
68
75
  # delimiter - the delimiter to separate the number for each level
69
76
  # append - the String to append at the end of the section number
@@ -103,10 +110,12 @@ class Section < AbstractBlock
103
110
  # Returns the section number as a String
104
111
  def sectnum(delimiter = '.', append = nil)
105
112
  append ||= (append == false ? '' : delimiter)
106
- if @level && @level > 1 && @parent && @parent.context == :section
107
- %(#{@parent.sectnum(delimiter)}#{@number}#{append})
108
- else
113
+ if @level == 1
109
114
  %(#{@number}#{append})
115
+ elsif @level > 1
116
+ Section === @parent ? %(#{@parent.sectnum(delimiter)}#{@number}#{append}) : %(#{@number}#{append})
117
+ else # @level == 0
118
+ %(#{Helpers.int_to_roman @number}#{append})
110
119
  end
111
120
  end
112
121
 
@@ -153,7 +162,7 @@ class Section < AbstractBlock
153
162
  #
154
163
  # Returns The parent Block
155
164
  def << block
156
- enumerate_section block if block.context == :section
165
+ assign_numeral block if block.context == :section
157
166
  super
158
167
  end
159
168
 
@@ -169,32 +178,40 @@ class Section < AbstractBlock
169
178
  # Public: Generate a String ID from the given section title.
170
179
  #
171
180
  # The generated ID is prefixed with value of the 'idprefix' attribute, which
172
- # is an underscore by default. Invalid characters are replaced with the
173
- # value of the 'idseparator' attribute, which is an underscore by default.
181
+ # is an underscore (_) by default. Invalid characters are then removed and
182
+ # spaces are replaced with the value of the 'idseparator' attribute, which is
183
+ # an underscore (_) by default.
174
184
  #
175
- # If the generated ID is already in use in the document, a count is appended
176
- # until a unique id is found.
185
+ # If the generated ID is already in use in the document, a count is appended,
186
+ # offset by the separator, until a unique ID is found.
177
187
  #
178
- # Section ID generation can be disabled by undefining the 'sectids' attribute.
188
+ # Section ID generation can be disabled by unsetting the 'sectids' document attribute.
179
189
  #
180
190
  # Examples
181
191
  #
182
192
  # Section.generate_id 'Foo', document
183
193
  # => "_foo"
184
194
  #
195
+ # Returns the generated [String] ID.
185
196
  def self.generate_id title, document
186
197
  attrs = document.attributes
187
- sep = attrs['idseparator'] || '_'
188
198
  pre = attrs['idprefix'] || '_'
189
- gen_id = %(#{pre}#{title.downcase.gsub InvalidSectionIdCharsRx, sep})
190
- unless sep.empty?
191
- # remove repeat and trailing separator characters
192
- gen_id = gen_id.tr_s sep, sep
193
- gen_id = gen_id.chop if gen_id.end_with? sep
194
- # ensure id doesn't begin with idseparator if idprefix is empty and idseparator is not empty
195
- if pre.empty?
196
- gen_id = gen_id.slice 1, gen_id.length while gen_id.start_with? sep
199
+ if (sep = attrs['idseparator'])
200
+ if sep.length == 1 || (!(no_sep = sep.empty?) && (sep = attrs['idseparator'] = sep.chr))
201
+ sep_sub = sep == '-' || sep == '.' ? ' .-' : %( #{sep}.-)
197
202
  end
203
+ else
204
+ sep, sep_sub = '_', ' _.-'
205
+ end
206
+ gen_id = %(#{pre}#{title.downcase.gsub InvalidSectionIdCharsRx, ''})
207
+ if no_sep
208
+ gen_id = gen_id.delete ' '
209
+ else
210
+ # replace space with separator and remove repeating and trailing separator characters
211
+ gen_id = gen_id.tr_s sep_sub, sep
212
+ gen_id = gen_id.chop if gen_id.end_with? sep
213
+ # ensure id doesn't begin with idseparator if idprefix is empty (assuming idseparator is not empty)
214
+ gen_id = gen_id.slice 1, gen_id.length if pre.empty? && (gen_id.start_with? sep)
198
215
  end
199
216
  if document.catalog[:ids].key? gen_id
200
217
  ids, cnt = document.catalog[:ids], Compliance.unique_id_start_index
@@ -8,6 +8,7 @@ class Stylesheets
8
8
  DEFAULT_STYLESHEET_NAME = 'asciidoctor.css'
9
9
  DEFAULT_PYGMENTS_STYLE = 'default'
10
10
  STYLESHEETS_DATA_PATH = ::File.join DATA_PATH, 'stylesheets'
11
+ PygmentsBgColorRx = /^\.pygments +\{ *background: *([^;]+);/
11
12
 
12
13
  @__instance__ = new
13
14
 
@@ -23,7 +24,7 @@ class Stylesheets
23
24
  #
24
25
  # returns the [String] Asciidoctor stylesheet data
25
26
  def primary_stylesheet_data
26
- @primary_stylesheet_data ||= ::IO.read(::File.join(STYLESHEETS_DATA_PATH, 'asciidoctor-default.css')).chomp
27
+ @primary_stylesheet_data ||= ::IO.read(::File.join(STYLESHEETS_DATA_PATH, 'asciidoctor-default.css')).rstrip
27
28
  end
28
29
 
29
30
  def embed_primary_stylesheet
@@ -48,7 +49,7 @@ class Stylesheets
48
49
  # unless load_coderay.nil?
49
50
  # ::CodeRay::Encoders[:html]::CSS.new(:default).stylesheet
50
51
  # end
51
- @coderay_stylesheet_data ||= ::IO.read(::File.join(STYLESHEETS_DATA_PATH, 'coderay-asciidoctor.css')).chomp
52
+ @coderay_stylesheet_data ||= ::IO.read(::File.join(STYLESHEETS_DATA_PATH, 'coderay-asciidoctor.css')).rstrip
52
53
  end
53
54
 
54
55
  def embed_coderay_stylesheet
@@ -65,16 +66,23 @@ class Stylesheets
65
66
  %(pygments-#{style || DEFAULT_PYGMENTS_STYLE}.css)
66
67
  end
67
68
 
69
+ def pygments_background style = nil
70
+ if load_pygments && PygmentsBgColorRx =~ (::Pygments.css '.pygments', :style => style || DEFAULT_PYGMENTS_STYLE)
71
+ $1
72
+ end
73
+ end
74
+
68
75
  # Public: Generate the Pygments stylesheet with the specified style.
69
76
  #
70
77
  # returns the [String] Pygments stylesheet data
71
78
  def pygments_stylesheet_data style = nil
72
79
  if load_pygments
73
- (@pygments_stylesheet_data ||= {})[style || DEFAULT_PYGMENTS_STYLE] ||=
74
- (::Pygments.css '.listingblock .pygments', :classprefix => 'tok-', :style => (style || DEFAULT_PYGMENTS_STYLE)).
80
+ style ||= DEFAULT_PYGMENTS_STYLE
81
+ (@pygments_stylesheet_data ||= {})[style] ||=
82
+ ((::Pygments.css '.listingblock .pygments', :classprefix => 'tok-', :style => style) || '/* Failed to load Pygments CSS. */').
75
83
  sub('.listingblock .pygments {', '.listingblock .pygments, .listingblock .pygments code {')
76
84
  else
77
- '/* Pygments styles disabled. Pygments is not available. */'
85
+ '/* Pygments CSS disabled. Pygments is not available. */'
78
86
  end
79
87
  end
80
88
 
@@ -43,14 +43,24 @@ module Substitutors
43
43
 
44
44
  SUB_HIGHLIGHT = ['coderay', 'pygments']
45
45
 
46
- # Delimiters and matchers for the passthrough placeholder
47
- # See http://www.aivosto.com/vbtips/control-characters.html#listabout for characters to use
46
+ if ::RUBY_MIN_VERSION_1_9
47
+ CAN = %(\u0018)
48
+ DEL = %(\u007f)
48
49
 
49
- # SPA, start of guarded protected area (\u0096)
50
- PASS_START = %(\u0096)
50
+ # Delimiters and matchers for the passthrough placeholder
51
+ # See http://www.aivosto.com/vbtips/control-characters.html#listabout for characters to use
51
52
 
52
- # EPA, end of guarded protected area (\u0097)
53
- PASS_END = %(\u0097)
53
+ # SPA, start of guarded protected area (\u0096)
54
+ PASS_START = %(\u0096)
55
+
56
+ # EPA, end of guarded protected area (\u0097)
57
+ PASS_END = %(\u0097)
58
+ else
59
+ CAN = 24.chr
60
+ DEL = 127.chr
61
+ PASS_START = 150.chr
62
+ PASS_END = 151.chr
63
+ end
54
64
 
55
65
  # match passthrough slot
56
66
  PassSlotRx = /#{PASS_START}(\d+)#{PASS_END}/
@@ -74,39 +84,20 @@ module Substitutors
74
84
  # Internal: A String Array of passthough (unprocessed) text captured from this block
75
85
  attr_reader :passthroughs
76
86
 
77
- # Public: Apply the specified substitutions to the source.
87
+ # Public: Apply the specified substitutions to the text.
78
88
  #
79
- # source - The String or String Array of text to process; must not be nil.
80
- # subs - The substitutions to perform; can be a Symbol, Symbol Array or nil (default: NORMAL_SUBS).
81
- # expand - A Boolean (or nil) to control whether substitution aliases are expanded (default: nil).
89
+ # text - The String or String Array of text to process; must not be nil.
90
+ # subs - The substitutions to perform; must be a Symbol Array or nil (default: NORMAL_SUBS).
82
91
  #
83
- # Returns a String or String Array with substitutions applied, matching the type of source argument.
84
- def apply_subs source, subs = NORMAL_SUBS, expand = nil
85
- if source.empty? || !subs
86
- return source
87
- elsif expand
88
- if ::Symbol === subs
89
- subs = SUB_GROUPS[subs] || [subs]
90
- else
91
- effective_subs = []
92
- subs.each do |key|
93
- if (sub_group = SUB_GROUPS[key])
94
- effective_subs += sub_group unless sub_group.empty?
95
- else
96
- effective_subs << key
97
- end
98
- end
92
+ # Returns a String or String Array to match the type of the text argument with substitutions applied.
93
+ def apply_subs text, subs = NORMAL_SUBS
94
+ return text if text.empty? || !subs
99
95
 
100
- if (subs = effective_subs).empty?
101
- return source
102
- end
103
- end
104
- elsif subs.empty?
105
- return source
96
+ if (multiline = ::Array === text)
97
+ #text = text.size > 1 ? (text.join LF) : text[0]
98
+ text = text[1] ? (text.join LF) : text[0]
106
99
  end
107
100
 
108
- text = (multiline = ::Array === source) ? source * LF : source
109
-
110
101
  if (has_passthroughs = subs.include? :macros)
111
102
  text = extract_passthroughs text
112
103
  has_passthroughs = false if @passthroughs.empty?
@@ -119,7 +110,7 @@ module Substitutors
119
110
  when :quotes
120
111
  text = sub_quotes text
121
112
  when :attributes
122
- text = sub_attributes(text.split LF, -1) * LF if text.include? ATTR_REF_HEAD
113
+ text = sub_attributes text if text.include? ATTR_REF_HEAD
123
114
  when :replacements
124
115
  text = sub_replacements text
125
116
  when :macros
@@ -131,7 +122,7 @@ module Substitutors
131
122
  when :post_replacements
132
123
  text = sub_post_replacements text
133
124
  else
134
- warn %(asciidoctor: WARNING: unknown substitution type #{type})
125
+ logger.warn %(unknown substitution type #{type})
135
126
  end
136
127
  end
137
128
  text = restore_passthroughs text if has_passthroughs
@@ -238,13 +229,13 @@ module Substitutors
238
229
  next m[0][1..-1]
239
230
  end
240
231
 
241
- @passthroughs[pass_key = @passthroughs.size] = {:text => (unescape_brackets m[8]), :subs => (m[7] ? (resolve_pass_subs m[7]) : [])}
232
+ @passthroughs[pass_key = @passthroughs.size] = {:text => (unescape_brackets m[8]), :subs => (m[7] ? (resolve_pass_subs m[7]) : nil)}
242
233
  end
243
234
 
244
235
  %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END})
245
236
  } if (text.include? '++') || (text.include? '$$') || (text.include? 'ss:')
246
237
 
247
- pass_inline_char1, pass_inline_char2, pass_inline_rx = PassInlineRx[compat_mode]
238
+ pass_inline_char1, pass_inline_char2, pass_inline_rx = InlinePassRx[compat_mode]
248
239
  text = text.gsub(pass_inline_rx) {
249
240
  # alias match for Ruby 1.8.7 compat
250
241
  m = $~
@@ -264,7 +255,8 @@ module Substitutors
264
255
 
265
256
  if attributes
266
257
  if format_mark == '`' && !old_behavior
267
- next %(#{preceding}[#{attributes}]#{escape_mark}`#{extract_passthroughs content}`)
258
+ # extract nested single-plus passthrough; otherwise return unprocessed
259
+ next (extract_inner_passthrough content, %(#{preceding}[#{attributes}]#{escape_mark}), attributes)
268
260
  end
269
261
 
270
262
  if escape_mark
@@ -278,7 +270,8 @@ module Substitutors
278
270
  attributes = parse_attributes attributes
279
271
  end
280
272
  elsif format_mark == '`' && !old_behavior
281
- next %(#{preceding}#{escape_mark}`#{extract_passthroughs content}`)
273
+ # extract nested single-plus passthrough; otherwise return unprocessed
274
+ next (extract_inner_passthrough content, %(#{preceding}#{escape_mark}))
282
275
  elsif escape_mark
283
276
  # honor the escape of the formatting mark
284
277
  next %(#{preceding}#{m[3][1..-1]})
@@ -311,10 +304,10 @@ module Substitutors
311
304
  end
312
305
 
313
306
  if (type = m[1].to_sym) == :stem
314
- type = ((default_stem_type = @document.attributes['stem']).nil_or_empty? ? 'asciimath' : default_stem_type).to_sym
307
+ type = STEM_TYPE_ALIASES[@document.attributes['stem']].to_sym
315
308
  end
316
309
  content = unescape_brackets m[3]
317
- subs = m[2] ? (resolve_pass_subs m[2]) : ((@document.basebackend? 'html') ? BASIC_SUBS : [])
310
+ subs = m[2] ? (resolve_pass_subs m[2]) : ((@document.basebackend? 'html') ? BASIC_SUBS : nil)
318
311
  @passthroughs[pass_key = @passthroughs.size] = {:text => content, :subs => subs, :type => type}
319
312
  %(#{PASS_START}#{pass_key}#{PASS_END})
320
313
  } if (text.include? ':') && ((text.include? 'stem:') || (text.include? 'math:'))
@@ -322,6 +315,21 @@ module Substitutors
322
315
  text
323
316
  end
324
317
 
318
+ def extract_inner_passthrough text, pre, attributes = nil
319
+ if (text.end_with? '+') && (text.start_with? '+', '\+') && SinglePlusInlinePassRx =~ text
320
+ if $1
321
+ %(#{pre}`+#{$2}+`)
322
+ else
323
+ @passthroughs[pass_key = @passthroughs.size] = attributes ?
324
+ { :text => $2, :subs => BASIC_SUBS, :attributes => attributes, :type => :unquoted } :
325
+ { :text => $2, :subs => BASIC_SUBS }
326
+ %(#{pre}`#{PASS_START}#{pass_key}#{PASS_END}`)
327
+ end
328
+ else
329
+ %(#{pre}`#{text}`)
330
+ end
331
+ end
332
+
325
333
  # Internal: Restore the passthrough text by reinserting into the placeholder positions
326
334
  #
327
335
  # text - The String text into which to restore the passthrough text
@@ -366,10 +374,6 @@ module Substitutors
366
374
  end
367
375
  text
368
376
  end
369
-
370
- def sub_specialchars text
371
- (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub SpecialCharsRx, SpecialCharsTr) : text
372
- end
373
377
  else
374
378
  # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc)
375
379
  #
@@ -404,23 +408,23 @@ module Substitutors
404
408
  end
405
409
  text
406
410
  end
411
+ end
407
412
 
408
- # Public: Substitute special characters (i.e., encode XML)
409
- #
410
- # The special characters are <, &, and >, which get replaced with &lt;,
411
- # &amp;, and &gt;, respectively.
412
- #
413
- # text - The String text to process
414
- #
415
- # returns The String text with special characters replaced
416
- if ::RUBY_MIN_VERSION_1_9
417
- def sub_specialchars text
418
- (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub! SpecialCharsRx, SpecialCharsTr) : text
419
- end
420
- else
421
- def sub_specialchars text
422
- (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub!(SpecialCharsRx) { SpecialCharsTr[$&] }) : text
423
- end
413
+ # Public: Substitute special characters (i.e., encode XML)
414
+ #
415
+ # The special characters <, &, and > get replaced with &lt;,
416
+ # &amp;, and &gt;, respectively.
417
+ #
418
+ # text - The String text to process.
419
+ #
420
+ # returns The String text with special characters replaced.
421
+ if ::RUBY_MIN_VERSION_1_9
422
+ def sub_specialchars text
423
+ (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub SpecialCharsRx, SpecialCharsTr) : text
424
+ end
425
+ else
426
+ def sub_specialchars text
427
+ (text.include? '<') || (text.include? '&') || (text.include? '>') ? (text.gsub(SpecialCharsRx) { SpecialCharsTr[$&] }) : text
424
428
  end
425
429
  end
426
430
  alias sub_specialcharacters sub_specialchars
@@ -444,74 +448,78 @@ module Substitutors
444
448
  end
445
449
  end
446
450
 
447
- # Public: Substitute attribute references
451
+ # Public: Substitutes attribute references in the specified text
448
452
  #
449
453
  # Attribute references are in the format +{name}+.
450
454
  #
451
- # If an attribute referenced in the line is missing, the line is dropped.
455
+ # If an attribute referenced in the line is missing or undefined, the line may be dropped
456
+ # based on the attribute-missing or attribute-undefined setting, respectively.
452
457
  #
453
- # text - The String text to process
458
+ # text - The String text to process
459
+ # opts - A Hash of options to control processing: (default: {})
460
+ # * :attribute_missing controls how to handle a missing attribute
454
461
  #
455
- # returns The String text with the attribute references replaced with attribute values
456
- #--
457
- # NOTE it's necessary to perform this substitution line-by-line
458
- # so that a missing key doesn't wipe out the whole block of data
459
- # when attribute-undefined and/or attribute-missing is drop-line
460
- def sub_attributes data, opts = {}
461
- # normalizes data type to an array (string becomes single-element array)
462
- data = [data] if (input_is_string = ::String === data)
463
- doc_attrs, result = @document.attributes, []
464
- data.each do |line|
465
- reject = reject_if_empty = false
466
- line = line.gsub(AttributeReferenceRx) {
467
- # escaped attribute, return unescaped
468
- if $1 == RS || $4 == RS
469
- %({#{$2}})
470
- elsif $3
471
- case (args = $2.split ':', 3).shift
472
- when 'set'
473
- _, value = Parser.store_attribute args[0], args[1] || '', @document
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 unless value
479
- reject_if_empty = true
480
- ''
481
- when 'counter2'
482
- @document.counter(*args)
483
- reject_if_empty = true
484
- ''
485
- else # 'counter'
486
- @document.counter(*args)
487
- end
488
- elsif doc_attrs.key?(key = $2.downcase)
489
- doc_attrs[key]
490
- elsif INTRINSIC_ATTRIBUTES.key? key
491
- INTRINSIC_ATTRIBUTES[key]
492
- else
493
- case (attribute_missing ||= opts[:attribute_missing] || (doc_attrs.fetch 'attribute-missing', Compliance.attribute_missing))
494
- when 'drop'
495
- # QUESTION should we warn in this case?
496
- reject_if_empty = true
497
- ''
498
- when 'drop-line'
499
- warn %(asciidoctor: WARNING: dropping line containing reference to missing attribute: #{key})
500
- reject = true
501
- break ''
502
- when 'warn'
503
- warn %(asciidoctor: WARNING: skipping reference to missing attribute: #{key})
504
- $&
505
- else # 'skip'
506
- $&
462
+ # Returns the [String] text with the attribute references replaced with resolved values
463
+ def sub_attributes text, opts = {}
464
+ doc_attrs = @document.attributes
465
+ drop = drop_line = drop_empty_line = attribute_undefined = attribute_missing = nil
466
+ result = text.gsub AttributeReferenceRx do
467
+ # escaped attribute, return unescaped
468
+ if $1 == RS || $4 == RS
469
+ %({#{$2}})
470
+ elsif $3
471
+ case (args = $2.split ':', 3).shift
472
+ when 'set'
473
+ _, value = Parser.store_attribute args[0], args[1] || '', @document
474
+ # NOTE since this is an assignment, only drop-line applies here (skip and drop imply the same result)
475
+ if value || (attribute_undefined ||= doc_attrs['attribute-undefined'] || Compliance.attribute_undefined) != 'drop-line'
476
+ drop = drop_empty_line = DEL
477
+ else
478
+ drop = drop_line = CAN
507
479
  end
480
+ when 'counter2'
481
+ @document.counter(*args)
482
+ drop = drop_empty_line = DEL
483
+ else # 'counter'
484
+ @document.counter(*args)
508
485
  end
509
- } if line.include? ATTR_REF_HEAD
510
-
511
- result << line unless reject || (reject_if_empty && line.empty?)
486
+ elsif doc_attrs.key?(key = $2.downcase)
487
+ doc_attrs[key]
488
+ elsif (value = INTRINSIC_ATTRIBUTES[key])
489
+ value
490
+ else
491
+ case (attribute_missing ||= opts[:attribute_missing] || doc_attrs['attribute-missing'] || Compliance.attribute_missing)
492
+ when 'drop'
493
+ drop = drop_empty_line = DEL
494
+ when 'drop-line'
495
+ logger.warn %(dropping line containing reference to missing attribute: #{key})
496
+ drop = drop_line = CAN
497
+ when 'warn'
498
+ logger.warn %(skipping reference to missing attribute: #{key})
499
+ $&
500
+ else # 'skip'
501
+ $&
502
+ end
503
+ end
512
504
  end
513
505
 
514
- input_is_string ? result * LF : result
506
+ if drop
507
+ # drop lines from result
508
+ if drop_empty_line
509
+ lines = (result.tr_s DEL, DEL).split LF, -1
510
+ if drop_line
511
+ (lines.reject {|line| line == DEL || line == CAN || (line.start_with? CAN) || (line.include? CAN) }.join LF).delete DEL
512
+ else
513
+ (lines.reject {|line| line == DEL }.join LF).delete DEL
514
+ end
515
+ elsif result.include? LF
516
+ (result.split LF, -1).reject {|line| line == CAN || (line.start_with? CAN) || (line.include? CAN) }.join LF
517
+ else
518
+ ''
519
+ end
520
+ else
521
+ result
522
+ end
515
523
  end
516
524
 
517
525
  # Public: Substitute inline macros (e.g., links, images, etc)
@@ -529,8 +537,7 @@ module Substitutors
529
537
  found_colon = source.include? ':'
530
538
  found_macroish = found[:macroish] = found_square_bracket && found_colon
531
539
  found_macroish_short = found_macroish && (source.include? ':[')
532
- doc_attrs = @document.attributes
533
- use_link_attrs = doc_attrs.key? 'linkattrs'
540
+ doc_attrs = (doc = @document).attributes
534
541
  result = source
535
542
 
536
543
  if doc_attrs.key? 'experimental'
@@ -591,7 +598,7 @@ module Substitutors
591
598
  end
592
599
 
593
600
  if (result.include? '"') && (result.include? '&gt;')
594
- result = result.gsub(MenuInlineRx) {
601
+ result = result.gsub(InlineMenuRx) {
595
602
  # alias match for Ruby 1.8.7 compat
596
603
  m = $~
597
604
  # honor the escape
@@ -610,7 +617,7 @@ module Substitutors
610
617
 
611
618
  # FIXME this location is somewhat arbitrary, probably need to be able to control ordering
612
619
  # TODO this handling needs some cleanup
613
- if (extensions = @document.extensions) && extensions.inline_macros? # && found_macroish
620
+ if (extensions = doc.extensions) && extensions.inline_macros? # && found_macroish
614
621
  extensions.inline_macros.each do |extension|
615
622
  result = result.gsub(extension.instance.regexp) {
616
623
  # alias match for Ruby 1.8.7 compat
@@ -664,63 +671,72 @@ module Substitutors
664
671
  # TODO remove this special case once titles use normal substitution order
665
672
  target = sub_attributes target
666
673
  end
667
- @document.register(:images, target) unless type == 'icon'
674
+ doc.register(:images, target) unless type == 'icon'
668
675
  attrs = parse_attributes(m[2], posattrs, :unescape_input => true)
669
676
  attrs['alt'] ||= (attrs['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
670
677
  Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).convert
671
678
  }
672
679
  end
673
680
 
674
- if ((result.include? '((') && (result.include? '))')) ||
675
- (found_macroish_short && (result.include? 'indexterm'))
681
+ if ((result.include? '((') && (result.include? '))')) || (found_macroish_short && (result.include? 'dexterm'))
676
682
  # (((Tigers,Big cats)))
677
683
  # indexterm:[Tigers,Big cats]
678
684
  # ((Tigers))
679
685
  # indexterm2:[Tigers]
680
686
  result = result.gsub(InlineIndextermMacroRx) {
681
- # alias match for Ruby 1.8.7 compat
682
- m = $~
683
-
684
- # honor the escape
685
- if m[0].start_with? RS
686
- next m[0][1..-1]
687
- end
688
-
689
- case m[1]
687
+ case $1
690
688
  when 'indexterm'
689
+ text = $2
690
+ # honor the escape
691
+ if (m0 = $&).start_with? RS
692
+ next m0.slice 1, m0.length
693
+ end
691
694
  # indexterm:[Tigers,Big cats]
692
- terms = split_simple_csv(normalize_string m[2], true)
693
- @document.register :indexterms, terms
695
+ terms = split_simple_csv normalize_string text, true
696
+ doc.register :indexterms, terms
694
697
  (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
695
698
  when 'indexterm2'
699
+ text = $2
700
+ # honor the escape
701
+ if (m0 = $&).start_with? RS
702
+ next m0.slice 1, m0.length
703
+ end
696
704
  # indexterm2:[Tigers]
697
- term = normalize_string m[2], true
698
- @document.register :indexterms, [term]
705
+ term = normalize_string text, true
706
+ doc.register :indexterms, [term]
699
707
  (Inline.new self, :indexterm, term, :type => :visible).convert
700
708
  else
701
- text, visible, before, after = m[3], true, nil, nil
702
- if text.start_with? '('
703
- if text.end_with? ')'
704
- text, visible = (text.slice 1, text.length - 2), false
709
+ text = $3
710
+ # honor the escape
711
+ if (m0 = $&).start_with? RS
712
+ # escape concealed index term, but process nested flow index term
713
+ if (text.start_with? '(') && (text.end_with? ')')
714
+ text = text.slice 1, text.length - 2
715
+ visible, before, after = true, '(', ')'
705
716
  else
706
- text, before, after = (text.slice 1, text.length - 1), '(', ''
717
+ next m0.slice 1, m0.length
707
718
  end
708
- elsif text.end_with? ')'
719
+ else
720
+ visible = true
709
721
  if text.start_with? '('
710
- text, visible = (text.slice 1, text.length - 2), false
711
- else
722
+ if text.end_with? ')'
723
+ text, visible = (text.slice 1, text.length - 2), false
724
+ else
725
+ text, before, after = (text.slice 1, text.length), '(', ''
726
+ end
727
+ elsif text.end_with? ')'
712
728
  text, before, after = (text.slice 0, text.length - 1), '', ')'
713
729
  end
714
730
  end
715
731
  if visible
716
732
  # ((Tigers))
717
733
  term = normalize_string text
718
- @document.register :indexterms, [term]
734
+ doc.register :indexterms, [term]
719
735
  result = (Inline.new self, :indexterm, term, :type => :visible).convert
720
736
  else
721
737
  # (((Tigers,Big cats)))
722
738
  terms = split_simple_csv(normalize_string text)
723
- @document.register :indexterms, terms
739
+ doc.register :indexterms, terms
724
740
  result = (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
725
741
  end
726
742
  before ? %(#{before}#{result}#{after}) : result
@@ -730,7 +746,7 @@ module Substitutors
730
746
 
731
747
  if found_colon && (result.include? '://')
732
748
  # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>)
733
- result = result.gsub(LinkInlineRx) {
749
+ result = result.gsub(InlineLinkRx) {
734
750
  # alias match for Ruby 1.8.7 compat
735
751
  m = $~
736
752
  # honor the escape
@@ -759,15 +775,13 @@ module Substitutors
759
775
  if prefix.start_with?('&lt;') && target.end_with?('&gt;')
760
776
  prefix = prefix[4..-1]
761
777
  target = target[0...-4]
778
+ # strip trailing ;
779
+ # check for trailing );
780
+ elsif (target = target.chop).end_with?(')')
781
+ target = target.chop
782
+ suffix = ');'
762
783
  else
763
- # strip trailing ;
764
- # check for trailing );
765
- if (target = target.chop).end_with?(')')
766
- target = target.chop
767
- suffix = ');'
768
- else
769
- suffix = ';'
770
- end
784
+ suffix = ';'
771
785
  end
772
786
  when ':'
773
787
  # strip trailing :
@@ -779,15 +793,16 @@ module Substitutors
779
793
  suffix = ':'
780
794
  end
781
795
  end
796
+ # NOTE handle case when remaining target is a URI scheme (e.g., http://)
797
+ return m[0] if target.end_with? '://'
782
798
  end
783
799
 
784
800
  attrs, link_opts = nil, { :type => :link }
785
801
  unless text.empty?
786
802
  text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
787
- if use_link_attrs && ((text.start_with? '"') || ((text.include? ',') && (text.include? '=')))
788
- attrs = parse_attributes text, []
803
+ if !doc.compat_mode && (text.include? '=')
804
+ text = (attrs = (AttributeList.new text, self).parse)[1] || ''
789
805
  link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
790
- text = attrs[1] || ''
791
806
  end
792
807
 
793
808
  # TODO enable in Asciidoctor 1.6.x
@@ -818,7 +833,7 @@ module Substitutors
818
833
  end
819
834
  end
820
835
 
821
- @document.register :links, (link_opts[:target] = target)
836
+ doc.register :links, (link_opts[:target] = target)
822
837
  link_opts[:attributes] = attrs if attrs
823
838
  %(#{prefix}#{Inline.new(self, :anchor, text, link_opts).convert}#{suffix})
824
839
  }
@@ -837,10 +852,10 @@ module Substitutors
837
852
  attrs, link_opts = nil, { :type => :link }
838
853
  unless (text = m[3]).empty?
839
854
  text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
840
- if use_link_attrs && ((text.start_with? '"') || ((text.include? ',') && (mailto || (text.include? '='))))
841
- attrs = parse_attributes text, []
842
- link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
843
- if mailto
855
+ if mailto
856
+ if !doc.compat_mode && (text.include? ',')
857
+ text = (attrs = (AttributeList.new text, self).parse)[1] || ''
858
+ link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
844
859
  if attrs.key? 2
845
860
  if attrs.key? 3
846
861
  target = %(#{target}?subject=#{Helpers.uri_encode attrs[2]}&amp;body=#{Helpers.uri_encode attrs[3]})
@@ -849,7 +864,9 @@ module Substitutors
849
864
  end
850
865
  end
851
866
  end
852
- text = attrs[1] || ''
867
+ elsif !doc.compat_mode && (text.include? '=')
868
+ text = (attrs = (AttributeList.new text, self).parse)[1] || ''
869
+ link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
853
870
  end
854
871
 
855
872
  # TODO enable in Asciidoctor 1.6.x
@@ -886,14 +903,14 @@ module Substitutors
886
903
  end
887
904
 
888
905
  # QUESTION should a mailto be registered as an e-mail address?
889
- @document.register :links, (link_opts[:target] = target)
906
+ doc.register :links, (link_opts[:target] = target)
890
907
  link_opts[:attributes] = attrs if attrs
891
908
  Inline.new(self, :anchor, text, link_opts).convert
892
909
  }
893
910
  end
894
911
 
895
912
  if result.include? '@'
896
- result = result.gsub(EmailInlineRx) {
913
+ result = result.gsub(InlineEmailRx) {
897
914
  address, tip = $&, $1
898
915
  if tip
899
916
  next (tip == RS ? address[1..-1] : address)
@@ -901,13 +918,13 @@ module Substitutors
901
918
 
902
919
  target = %(mailto:#{address})
903
920
  # QUESTION should this be registered as an e-mail address?
904
- @document.register(:links, target)
921
+ doc.register(:links, target)
905
922
 
906
923
  Inline.new(self, :anchor, address, :type => :link, :target => target).convert
907
924
  }
908
925
  end
909
926
 
910
- if found_macroish_short && (result.include? 'footnote')
927
+ if found_macroish && (result.include? 'tnote')
911
928
  result = result.gsub(InlineFootnoteMacroRx) {
912
929
  # alias match for Ruby 1.8.7 compat
913
930
  m = $~
@@ -915,36 +932,35 @@ module Substitutors
915
932
  if m[0].start_with? RS
916
933
  next m[0][1..-1]
917
934
  end
918
- if m[1] == 'footnote'
919
- id = nil
920
- # REVIEW it's a dirty job, but somebody's gotta do it
921
- text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string m[2], true)), false)
922
- index = @document.counter('footnote-number')
923
- @document.register(:footnotes, Document::Footnote.new(index, id, text))
924
- type = nil
925
- target = nil
935
+ if m[1] # footnoteref (legacy)
936
+ id, text = (m[3] || '').split(',', 2)
926
937
  else
927
- id, text = m[2].split(',', 2)
928
- id = id.strip
938
+ id, text = m[2], m[3]
939
+ end
940
+ if id
929
941
  if text
930
942
  # REVIEW it's a dirty job, but somebody's gotta do it
931
943
  text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string text, true)), false)
932
- index = @document.counter('footnote-number')
933
- @document.register(:footnotes, Document::Footnote.new(index, id, text))
934
- type = :ref
935
- target = nil
944
+ index = doc.counter('footnote-number')
945
+ doc.register(:footnotes, Document::Footnote.new(index, id, text))
946
+ type, target = :ref, nil
936
947
  else
937
- if (footnote = @document.footnotes.find {|fn| fn.id == id })
938
- index = footnote.index
939
- text = footnote.text
948
+ if (footnote = doc.footnotes.find {|candidate| candidate.id == id })
949
+ index, text = footnote.index, footnote.text
940
950
  else
941
- index = nil
942
- text = id
951
+ logger.warn %(invalid footnote reference: #{id})
952
+ index, text = nil, id
943
953
  end
944
- target = id
945
- id = nil
946
- type = :xref
954
+ type, target, id = :xref, id, nil
947
955
  end
956
+ elsif text
957
+ # REVIEW it's a dirty job, but somebody's gotta do it
958
+ text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string text, true)), false)
959
+ index = doc.counter('footnote-number')
960
+ doc.register(:footnotes, Document::Footnote.new(index, id, text))
961
+ type = target = nil
962
+ else
963
+ next m[0]
948
964
  end
949
965
  Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).convert
950
966
  }
@@ -985,74 +1001,95 @@ module Substitutors
985
1001
  end
986
1002
 
987
1003
  # Internal: Substitute cross reference links
988
- def sub_inline_xrefs(text, found = nil)
989
- if ((found ? found[:macroish] : (text.include? '[')) && (text.include? 'xref:')) ||
990
- ((text.include? '&') && (text.include? '&lt;&lt;'))
991
- text = text.gsub(InlineXrefMacroRx) {
1004
+ def sub_inline_xrefs(content, found = nil)
1005
+ if ((found ? found[:macroish] : (content.include? '[')) && (content.include? 'xref:')) || ((content.include? '&') && (content.include? 'lt;&'))
1006
+ content = content.gsub(InlineXrefMacroRx) {
992
1007
  # alias match for Ruby 1.8.7 compat
993
1008
  m = $~
994
1009
  # honor the escape
995
1010
  if m[0].start_with? RS
996
1011
  next m[0][1..-1]
997
1012
  end
998
- if (id = m[1])
999
- id, reftext = id.split ',', 2
1000
- reftext = reftext.lstrip if reftext
1013
+ attrs, doc = {}, @document
1014
+ if (refid = m[1])
1015
+ refid, text = refid.split ',', 2
1016
+ text = text.lstrip if text
1001
1017
  else
1002
- id = m[2]
1003
- if (reftext = m[3]) && (reftext.include? R_SB)
1004
- reftext = reftext.gsub ESC_R_SB, R_SB
1018
+ macro = true
1019
+ refid = m[2]
1020
+ if (text = m[3])
1021
+ text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
1022
+ # NOTE if an equal sign (=) is present, parse text as attributes
1023
+ text = ((AttributeList.new text, self).parse_into attrs)[1] if !doc.compat_mode && (text.include? '=')
1005
1024
  end
1006
1025
  end
1007
1026
 
1008
- if (hash_idx = id.index '#')
1027
+ if doc.compat_mode
1028
+ fragment = refid
1029
+ elsif (hash_idx = refid.index '#')
1009
1030
  if hash_idx > 0
1010
- if (fragment_len = id.length - hash_idx - 1) > 0
1011
- path, fragment = (id.slice 0, hash_idx), (id.slice hash_idx + 1, fragment_len)
1031
+ if (fragment_len = refid.length - hash_idx - 1) > 0
1032
+ path, fragment = (refid.slice 0, hash_idx), (refid.slice hash_idx + 1, fragment_len)
1012
1033
  else
1013
- path, fragment = (id.slice 0, hash_idx), nil
1034
+ path = refid.slice 0, hash_idx
1035
+ end
1036
+ if (ext = ::File.extname path).empty?
1037
+ src2src = path
1038
+ elsif ASCIIDOC_EXTENSIONS[ext]
1039
+ src2src = (path = path.slice 0, path.length - ext.length)
1014
1040
  end
1015
1041
  else
1016
- target, path, fragment = id, nil, (id.slice 1, id.length)
1042
+ target, fragment = refid, (refid.slice 1, refid.length)
1017
1043
  end
1044
+ elsif macro && (refid.end_with? '.adoc')
1045
+ src2src = (path = refid.slice 0, refid.length - 5)
1018
1046
  else
1019
- path, fragment = nil, id
1047
+ fragment = refid
1020
1048
  end
1021
1049
 
1022
1050
  # handles: #id
1023
1051
  if target
1024
1052
  refid = fragment
1025
- # handles: path#, path.adoc#, path#id, or path.adoc#id
1053
+ logger.warn %(invalid reference: #{refid}) if $VERBOSE && !(doc.catalog[:ids].key? refid)
1026
1054
  elsif path
1027
- if (ext_idx = path.rindex '.') && ASCIIDOC_EXTENSIONS[path.slice ext_idx, path.length]
1028
- path = path.slice 0, ext_idx
1029
- end
1030
- # the referenced path is this document, or its contents has been included in this document
1031
- if @document.attributes['docname'] == path || @document.catalog[:includes].include?(path)
1032
- refid, path, target = fragment, nil, %(##{fragment})
1055
+ # handles: path#, path#id, path.adoc#, path.adoc#id, or path.adoc (xref macro only)
1056
+ # the referenced path is the current document, or its contents have been included in the current document
1057
+ if src2src && (doc.attributes['docname'] == path || doc.catalog[:includes][path])
1058
+ if fragment
1059
+ refid, path, target = fragment, nil, %(##{fragment})
1060
+ logger.warn %(invalid reference: #{refid}) if $VERBOSE && !(doc.catalog[:ids].key? refid)
1061
+ else
1062
+ refid, path, target = nil, nil, '#'
1063
+ end
1033
1064
  else
1034
- refid = fragment ? %(#{path}##{fragment}) : path
1035
- path = %(#{@document.attributes['relfileprefix']}#{path}#{@document.attributes.fetch 'outfilesuffix', '.html'})
1036
- target = fragment ? %(#{path}##{fragment}) : path
1037
- end
1038
- # handles: id or Section Title
1039
- else
1040
- # resolve fragment as reftext if it's not a known ID and resembles reftext (includes space or has uppercase char)
1041
- unless @document.catalog[:ids].key? fragment
1042
- if ((fragment.include? ' ') || fragment.downcase != fragment) &&
1043
- (resolved_id = @document.catalog[:ids].key fragment)
1044
- fragment = resolved_id
1045
- elsif $VERBOSE
1046
- warn %(asciidoctor: WARNING: invalid reference: #{fragment})
1065
+ refid, path = path, %(#{doc.attributes['relfileprefix']}#{path}#{src2src ? (doc.attributes.fetch 'relfilesuffix', doc.outfilesuffix) : ''})
1066
+ if fragment
1067
+ refid, target = %(#{refid}##{fragment}), %(#{path}##{fragment})
1068
+ else
1069
+ target = path
1047
1070
  end
1048
1071
  end
1072
+ # handles: id (in compat mode or when natural xrefs are disabled)
1073
+ elsif doc.compat_mode || !Compliance.natural_xrefs
1074
+ refid, target = fragment, %(##{fragment})
1075
+ logger.warn %(invalid reference: #{refid}) if $VERBOSE && !(doc.catalog[:ids].key? refid)
1076
+ # handles: id
1077
+ elsif doc.catalog[:ids].key? fragment
1078
+ refid, target = fragment, %(##{fragment})
1079
+ # handles: Node Title or Reference Text
1080
+ # do reverse lookup on fragment if not a known ID and resembles reftext (contains a space or uppercase char)
1081
+ elsif (refid = doc.catalog[:ids].key fragment) && ((fragment.include? ' ') || fragment.downcase != fragment)
1082
+ fragment, target = refid, %(##{refid})
1083
+ else
1049
1084
  refid, target = fragment, %(##{fragment})
1085
+ logger.warn %(invalid reference: #{refid}) if $VERBOSE
1050
1086
  end
1051
- Inline.new(self, :anchor, reftext, :type => :xref, :target => target, :attributes => {'path' => path, 'fragment' => fragment, 'refid' => refid}).convert
1087
+ attrs['path'], attrs['fragment'], attrs['refid'] = path, fragment, refid
1088
+ Inline.new(self, :anchor, text, :type => :xref, :target => target, :attributes => attrs).convert
1052
1089
  }
1053
1090
  end
1054
1091
 
1055
- text
1092
+ content
1056
1093
  end
1057
1094
 
1058
1095
  # Public: Substitute callout source references
@@ -1135,7 +1172,7 @@ module Substitutors
1135
1172
  # Returns a Hash of attributes (role and id only)
1136
1173
  def parse_quoted_text_attributes str
1137
1174
  # NOTE attributes are typically resolved after quoted text, so substitute eagerly
1138
- str = sub_attributes str if str.include? ATTR_REF_HEAD
1175
+ str = sub_attributes str, :multiline => true if str.include? ATTR_REF_HEAD
1139
1176
  # for compliance, only consider first positional attribute
1140
1177
  str = str.slice 0, (str.index ',') if str.include? ','
1141
1178
 
@@ -1178,8 +1215,8 @@ module Substitutors
1178
1215
  def parse_attributes(attrline, posattrs = ['role'], opts = {})
1179
1216
  return unless attrline
1180
1217
  return {} if attrline.empty?
1181
- attrline = @document.sub_attributes(attrline) if opts[:sub_input] && (attrline.include? ATTR_REF_HEAD)
1182
- attrline = unescape_bracketed_text(attrline) if opts[:unescape_input]
1218
+ attrline = @document.sub_attributes attrline if opts[:sub_input] && (attrline.include? ATTR_REF_HEAD)
1219
+ attrline = unescape_bracketed_text attrline if opts[:unescape_input]
1183
1220
  # substitutions are only performed on attribute values if block is not nil
1184
1221
  block = opts.fetch(:sub_result, true) ? self : nil
1185
1222
  if (into = opts[:into])
@@ -1189,7 +1226,33 @@ module Substitutors
1189
1226
  end
1190
1227
  end
1191
1228
 
1192
- # Internal: Strip bounding whitespace, fold endlines and unescaped closing
1229
+ # Expand all groups in the subs list and return. If no subs are resolve, return nil.
1230
+ #
1231
+ # subs - The substitutions to expand; can be a Symbol, Symbol Array or nil
1232
+ #
1233
+ # Returns a Symbol Array of substitutions to pass to apply_subs or nil if no substitutions were resolved.
1234
+ def expand_subs subs
1235
+ if ::Symbol === subs
1236
+ unless subs == :none
1237
+ SUB_GROUPS[subs] || [subs]
1238
+ end
1239
+ else
1240
+ expanded_subs = []
1241
+ subs.each do |key|
1242
+ unless key == :none
1243
+ if (sub_group = SUB_GROUPS[key])
1244
+ expanded_subs += sub_group
1245
+ else
1246
+ expanded_subs << key
1247
+ end
1248
+ end
1249
+ end
1250
+
1251
+ expanded_subs.empty? ? nil : expanded_subs
1252
+ end
1253
+ end
1254
+
1255
+ # Internal: Strip bounding whitespace, fold endlines and unescape closing
1193
1256
  # square brackets from text extracted from brackets
1194
1257
  def unescape_bracketed_text text
1195
1258
  if (text = text.strip.tr LF, ' ').include? R_SB
@@ -1253,9 +1316,9 @@ module Substitutors
1253
1316
  #
1254
1317
  # subs - A comma-delimited String of substitution aliases
1255
1318
  #
1256
- # returns An Array of Symbols representing the substitution operation
1319
+ # returns An Array of Symbols representing the substitution operation or nothing if no subs are found.
1257
1320
  def resolve_subs subs, type = :block, defaults = nil, subject = nil
1258
- return [] if subs.nil_or_empty?
1321
+ return if subs.nil_or_empty?
1259
1322
  # QUESTION should we store candidates as a Set instead of an Array?
1260
1323
  candidates = nil
1261
1324
  subs = subs.delete ' ' if subs.include? ' '
@@ -1306,12 +1369,12 @@ module Substitutors
1306
1369
  candidates += resolved_keys
1307
1370
  end
1308
1371
  end
1309
- return [] unless candidates
1372
+ return unless candidates
1310
1373
  # weed out invalid options and remove duplicates (order is preserved; first occurence wins)
1311
1374
  resolved = candidates & SUB_OPTIONS[type]
1312
1375
  unless (candidates - resolved).empty?
1313
1376
  invalid = candidates - resolved
1314
- warn %(asciidoctor: WARNING: invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : nil}#{subject}: #{invalid * ', '})
1377
+ logger.warn %(invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : ''}#{subject}: #{invalid * ', '})
1315
1378
  end
1316
1379
  resolved
1317
1380
  end
@@ -1339,19 +1402,21 @@ module Substitutors
1339
1402
  def highlight_source source, process_callouts, highlighter = nil
1340
1403
  case (highlighter ||= @document.attributes['source-highlighter'])
1341
1404
  when 'coderay'
1342
- unless (highlighter_loaded = defined? ::CodeRay) || @document.attributes['coderay-unavailable']
1405
+ unless (highlighter_loaded = defined? ::CodeRay) ||
1406
+ (defined? @@coderay_unavailable) || @document.attributes['coderay-unavailable']
1343
1407
  if (Helpers.require_library 'coderay', true, :warn).nil?
1344
- # prevent further attempts to load CodeRay
1345
- @document.set_attr 'coderay-unavailable'
1408
+ # prevent further attempts to load CodeRay in this process
1409
+ @@coderay_unavailable = true
1346
1410
  else
1347
1411
  highlighter_loaded = true
1348
1412
  end
1349
1413
  end
1350
1414
  when 'pygments'
1351
- unless (highlighter_loaded = defined? ::Pygments) || @document.attributes['pygments-unavailable']
1415
+ unless (highlighter_loaded = defined? ::Pygments) ||
1416
+ (defined? @@pygments_unavailable) || @document.attributes['pygments-unavailable']
1352
1417
  if (Helpers.require_library 'pygments', 'pygments.rb', :warn).nil?
1353
- # prevent further attempts to load Pygments
1354
- @document.set_attr 'pygments-unavailable'
1418
+ # prevent further attempts to load Pygments in this process
1419
+ @@pygments_unavailable = true
1355
1420
  else
1356
1421
  highlighter_loaded = true
1357
1422
  end
@@ -1422,21 +1487,26 @@ module Substitutors
1422
1487
  opts[:hl_lines] = highlight_lines * ' '
1423
1488
  end
1424
1489
  end
1490
+ # NOTE highlight can return nil if something goes wrong; fallback to source if this happens
1425
1491
  # TODO we could add the line numbers in ourselves instead of having to strip out the junk
1426
1492
  if (attr? 'linenums', nil, false) && (opts[:linenos] = @document.attributes['pygments-linenums-mode'] || 'table') == 'table'
1427
1493
  linenums_mode = :table
1428
- result = lexer.highlight(source, :options => opts).sub(PygmentsWrapperDivRx, '\1').gsub(PygmentsWrapperPreRx, '\1')
1429
- else
1430
- if PygmentsWrapperPreRx =~ (result = lexer.highlight(source, :options => opts))
1494
+ if (result = lexer.highlight source, :options => opts)
1495
+ result = (result.sub PygmentsWrapperDivRx, '\1').gsub PygmentsWrapperPreRx, '\1'
1496
+ else
1497
+ result = sub_specialchars source
1498
+ end
1499
+ elsif (result = lexer.highlight source, :options => opts)
1500
+ if PygmentsWrapperPreRx =~ result
1431
1501
  result = $1
1432
1502
  end
1503
+ else
1504
+ result = sub_specialchars source
1433
1505
  end
1434
1506
  end
1435
1507
 
1436
1508
  # fix passthrough placeholders that got caught up in syntax highlighting
1437
- unless @passthroughs.empty?
1438
- result = result.gsub HighlightedPassSlotRx, %(#{PASS_START}\\1#{PASS_END})
1439
- end
1509
+ result = result.gsub HighlightedPassSlotRx, %(#{PASS_START}\\1#{PASS_END}) unless @passthroughs.empty?
1440
1510
 
1441
1511
  if process_callouts && callout_marks
1442
1512
  lineno = 0
@@ -1539,7 +1609,11 @@ module Substitutors
1539
1609
  end
1540
1610
  end
1541
1611
 
1542
- @subs = (custom_subs = @attributes['subs']) ? (resolve_block_subs custom_subs, default_subs, @context) : default_subs.dup
1612
+ if (custom_subs = @attributes['subs'])
1613
+ @subs = (resolve_block_subs custom_subs, default_subs, @context) || []
1614
+ else
1615
+ @subs = default_subs.dup
1616
+ end
1543
1617
 
1544
1618
  # QUESION delegate this logic to a method?
1545
1619
  if @context == :listing && @style == 'source' && (@attributes.key? 'language') && (@document.basebackend? 'html') &&